社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  DATABASE

MySQL 官方承认 Bug,8.0.22 之前你的时间可能是错的

脚本之家 • 1 月前 • 167 次点击  
将 脚本之家 设为“星标⭐”
第一时间收到文章更新
学习, 学校, 大学, 纽约市, 公寓, 眼镜, 植物, 笔记本电脑

来源公众号:业余草(id:yyucao)

不少 Java 老司机可能都被时间类业务坑过,也有不少互联网大厂犯过这类错误还登上了热搜。

最近我司的一名年轻程序员也跳进了时间戳这类存储、读取、转换的坑里。好在有 AI,有不少老程序员能够帮他迅速定位问题,才不至于产生大麻烦。

所以,接下来,我们就一起来看看,MySQL 官方也承认的 Bug,8.0.22 之前,你的时间可能是错的。

MySQL 8 驱动区参数

我估计,大部分 Java 开发者都抄过这样一段连接串。

jdbc:mysql://localhost:3306/xttblog?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull

但这其中的各个参数是什么意思?serverTimezone 又到底做了什么?为什么 MySQL 8 的驱动“逼”着我们必须配置它?那些差 8 小时、差 13 小时、夏令时崩溃的诡异 Bug 又是怎么来的?接下来我们就把这件事彻底讲清楚吧。

配时区的坑

为什么 MySQL 8 的驱动要“逼”着我们配时区?

如果你从 MySQL 5.7 + Connector/J 5.1 升级到 MySQL 8 + Connector/J 8.x,大概率见过这个报错。

java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized
or represents more than one time zone.
You must configure either the server or JDBC driver (via the serverTimezone
configuration property)
 to use a more specifc time zone value if you want to
utilize time zone support.

注意那个乱码的 'Öйú±ê׼ʱ¼ä',它其实是“中国标准时间”被错误解码后的样子,是中文 Windows 上 MySQL 的 system_time_zone 返回值。这个报错几乎是每个国内开发者升级到 8.x 驱动后的“成人礼”,在阿里云开发者社区有大量同款提问记录。

背后的原因是 Connector/J 6.x/8.x 重构了时区处理逻辑,驱动在建立连接时需要明确知道“会话时区是什么”。官方文档的说法是,如果时区检测失败(主要原因是服务器使用了时区缩写),就必须设置显式时区,或在服务器上配置不同的时区。

于是就出现了文章开头那个著名的“教程式”解法,抄来一段 serverTimezone=UTC,程序能跑了,但某些业务数据却可能悄悄存在差了 8 个小时。

时区相关参数

如果只有一个时间或时区参数也就算了,关键是 mysql 的驱动现在要求的时区参数也太多了吧,有好几个。

connectionTimeZone

serverTimezone 和 connectionTimeZone 是同一类参数,也是最核心的参数。

只不过 serverTimezone 是老名字,从 Connector/J 8.0.23 起它就是 connectionTimeZone 的别名,官方已经明确表示“将来可能被废弃”。

connectionTimeZone 接受三种值,如下所示。

  • 具体时区。如 Asia/Shanghai、GMT+8(URL 里要写成 GMT%2B8);
  • LOCAL。假定连接时区与 JVM 默认时区相同(这也是 8.0.23 之后的默认值);
  • SERVER。让驱动尝试从 MySQL 会话变量 time_zone / system_time_zone 检测时区。

注意,它本身不会修改 MySQL 会话的 time_zone 变量,想改会话时区需要配合下一个参数。

forceConnectionTimeZoneToSession

forceConnectionTimeZoneToSession 是一个布尔类型的变量。当设置为 true 时,驱动会强制把 MySQL 会话时区替换为 connectionTimeZone 指定的值。

这个配置会影响 NOW()、CURRENT_TIMESTAMP 等数据库端函数的行为,所以它可能既是救命稻草,也可能是事故的来源。

preserveInstants

这是 mysql 8.0.23 新增的一个参数,也是一个非常关键的参数。

它控制驱动是否对 TIMESTAMP 类型做“瞬时时刻(instant)保留”转换,写入时把 JVM 时区的时刻换算成会话时区的字面值,读取时再换算回来。

DATETIME 与 TIMESTAMP
DATETIME 与 TIMESTAMP

它也是一个布尔类型的变量,默认 true,是 8.0.23 为修复 5.1 升级 8.0 的行为差异而引入的新机制。

同样的它也有坑需要注意。如果同时设置 connectionTimeZone=LOCAL、forceConnectionTimeZoneToSession=false、preserveInstants=false,那么 connectionTimeZone 就彻底失效了,因为官方文档明确说这种组合是无意义的。

useLegacyDatetimeCode

这是 5.1 时代的老参数,估计只有老程序员才记得。

因此它是 Connector/J 5.1 专用的一个参数。默认值也是 true,使用旧的日期处理代码,false 启用新的时区转换逻辑,大致等价于新驱动的 connectionTimeZone=SERVER&preserveInstants=true。

MySQL 8 驱动已经移除了这个参数,配置了也会被忽略掉,很多老项目升级后 URL 里还留着它,属于无害的“考古遗迹”,干扰项。

其它时间相关参数

参考链接 https://mysql.net.cn/doc/connector-j/en/connector-j-connp-props-datetime-types-processing.html 上的文档说明。还有不少其它时间、时区相关的参数配置,我就不一一细说了,下面再举两个常用的简单说一下。

  • zeroDateTimeBehavior:MySQL 允许 0000-00-00 00:00:00 这样的“零日期”,但 Java 无法表示。可选值:EXCEPTION(默认,抛异常)、 CONVERT_TO_NULL(转成 null)、ROUND(转成 0001-01-01)。生产环境一般设成 CONVERT_TO_NULL。
  • sendFractionalSeconds:是否发送小数秒(毫秒),默认 true,设成 false 会悄悄截断毫秒,偶尔引发“时间对不上”的灵异问题。

这两个用的不多,如果真有使用还得小心了再小心。

官方承认的 Bug

#30962953 是有官方 Release Notes 背书的真官方 Bug。MySQL 官方也在 8.0.23 的更新日志中写道。

从 Connector/J 5.1 升级到 8.0 后,保存再读取 DATETIME 和 TIMESTAMP 的结果有时会变得不同。这是因为 5.1 默认不保留时间瞬时,而 8.0.22 及更早版本会在发送前把时间戳转换到服务器会话时区。本次发布引入了新的时区转换控制机制,通过设置 preserveInstants=false 可恢复 5.1 的默认行为。(Bug #30962953, Bug #98695, Bug #30573281, Bug #95644)

也就是说,8.0.0 ~ 8.0.22 是一个“危险区间”。如果服务端时区配置不当(比如 CST),驱动会自动采用服务端时区做转换,触发上面的 13 小时事故。社区(bugstack、51CTO 等)总结的实践是,5.1 升级 8.x 时务必升到 8.0.23 及以上;永远显式指定 serverTimezone=Asia/Shanghai,不要依赖自动检测。

除此之外,还有 zeroDateTimeBehavior 枚举改名导致启动即报错等 bug。

比如,把驱动从 5.x 升到 8.x 后,应用直接起不来。

The connection property 'zeroDateTimeBehavior' acceptable values are:
'CONVERT_TO_NULL', 'EXCEPTION' or 'ROUND'.
The value  'convertToNull' is not acceptable.

5.x 时代大家写的都是 zeroDateTimeBehavior=convertToNull,8.x 把枚举值改成了全大写 CONVERT_TO_NULL,旧值直接被拒绝。修复就要直接改成 CONVERT_TO_NULL。

总之,关于时间与时区类的 bug,多的数不胜数。

TIMESTAMP 和 DATETIME

这两个时间类型,可以说是一切混乱的总根源。

  • TIMESTAMP:表示时间线上的一个“瞬间”。MySQL 内部始终以 UTC 存储,写入时按会话时区换算、读取时按会话时区还原。所以它对会话时区敏感,serverTimezone 和 forceConnectionTimeZoneToSession 主要就是在伺候它。
  • DATETIME:只是一个“墙钟字面值”,不带时区语义,存什么是什么。MariaDB 官方文档有个很精辟的警告,DATETIME 常被误用来存“时刻”,只要客户端和服务端时区一致就没事,一旦不一致就出事。

两者的区别,上面也有一张图画的很形象了,这里不再展开了。

最佳实践

瞎扯了这么多,都不够实用。

其实,只需要复制下面这个最佳实践的配置就行了。




    
# 连接串(国内业务)
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xttblog?\
useUnicode=true&characterEncoding=utf8&useSSL=true&\
serverTimezone=Asia/Shanghai&\
zeroDateTimeBehavior=CONVERT_TO_NULL&\
tinyInt1isBit=false

如果想更细致一点,看下面这 7 句最佳实践概括就行了。

  1. 驱动版本建议用 8.0.23+,5.1 的老项目非必要不用动,绕开官方的“危险区间”;
  2. serverTimezone 永远显式指定成 Asia/Shanghai 这类 IANA 名称,不要用 CST(歧义)、不要随手写 UTC(8 小时坑)、不要依赖自动检测(夏令时坑);
  3. 服务器、MySQL、JDBC 三方的时区配置要互相知晓、保持一致预期,上线前用 SELECT NOW() 和程序当前时间对一遍表;
  4. 需要数据库端时间(NOW())与 Java 端一致时,再考虑 forceConnectionTimeZoneToSession=true,并清楚它会修改会话时区;
  5. 零日期用 CONVERT_TO_NULL;毫秒别被 sendFractionalSeconds=false 吃掉;
  6. JSON 接口的时间还要检查 spring.jackson.time-zone;ORM 层(Hibernate 6)升级后回归测试时间字段;
  7. 存“时刻”用 TIMESTAMP + Instant/OffsetDateTime,存“字面值”(如用户预约的本地时间)用 DATETIME + LocalDateTime,不要混用。

结语

时区问题可大可小,本质上可能是一个三方协议问题,操作系统时区、MySQL 会话时区、JVM 时区,外加一个 JDBC 驱动做“翻译”。

不少老司机可能推荐大家使用 long 来存储时间戳,也是一种办法。也或者,时刻用 UTC + 绝对时间类型(PG timestamptz/ MySQL DATETIME(6) 或 BIGINT 毫秒),墙钟时间才用无时区类型;同时,MySQL 的 TIMESTAMP 记住两件事,Y2038 和会话时区转换。

图片
  推荐阅读:
  1. 为什么 Linux 要用 tar.gz,很少用 7Z 或 ZIP?
  2. 讲讲 Duo 作为科技产品命名的前世今生
  3. Windows史诗级更新!
  4. 乔布斯去世15年,亲儿子携10亿美元杀回硅谷:不做iPhone,誓把癌症做成慢性病!
  5. 吴恩达谈程序员的新分水岭:AI会写代码了,优秀工程师反而比以前更忙。还按2022年方式工作的人,已经危险了

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/201382