本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
【腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云
不少 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 时区的时刻换算成会话时区的字面值,读取时再换算回来。
它也是一个布尔类型的变量,默认 true,是 8.0.23 为修复 5.1 升级 8.0 的行为差异而引入的新机制。
同样的它也有坑需要注意。如果同时设置 connectionTimeZone=LOCAL、forceConnectionTimeZoneToSession=false、preserveInstants=false,那么 connectionTimeZone 就彻底失效了,因为官方文档明确说这种组合是无意义的。
文章多张配图参见我的公众号:https://mp.weixin.qq.com/s/PEJ7aBsNEMDUqy2TJXLWeA。
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 句最佳实践概括就行了。
- 驱动版本建议用 8.0.23+,5.1 的老项目非必要不用动,绕开官方的“危险区间”;
serverTimezone永远显式指定成Asia/Shanghai这类 IANA 名称,不要用CST(歧义)、不要随手写UTC(8 小时坑)、不要依赖自动检测(夏令时坑);- 服务器、MySQL、JDBC 三方的时区配置要互相知晓、保持一致预期,上线前用
SELECT NOW()和程序当前时间对一遍表; - 需要数据库端时间(
NOW())与 Java 端一致时,再考虑forceConnectionTimeZoneToSession=true,并清楚它会修改会话时区; - 零日期用
CONVERT_TO_NULL;毫秒别被sendFractionalSeconds=false吃掉; - JSON 接口的时间还要检查
spring.jackson.time-zone;ORM 层(Hibernate 6)升级后回归测试时间字段; - 存“时刻”用
TIMESTAMP+Instant/OffsetDateTime,存“字面值”(如用户预约的本地时间)用DATETIME+LocalDateTime,不要混用。
结语
时区问题可大可小,本质上可能是一个三方协议问题,操作系统时区、MySQL 会话时区、JVM 时区,外加一个 JDBC 驱动做“翻译”。
不少老司机可能推荐大家使用 long 来存储时间戳,也是一种办法。也或者,时刻用 UTC + 绝对时间类型(PG timestamptz/ MySQL DATETIME(6) 或 BIGINT 毫秒),墙钟时间才用无时区类型;同时,MySQL 的 TIMESTAMP 记住两件事,Y2038 和会话时区转换。

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!
本文原文出处:业余草: » MySQL 官方承认 Bug,8.0.22 之前你的时间可能是错的