写点什么

升级 mysql-connector-java-8.x 踩坑纪实

用户头像
小江
关注
发布于: 18 分钟前
升级mysql-connector-java-8.x踩坑纪实

案情重现 - 商品显示创建时间差了 13 个小时

最近公司在做云平台提供商的切换,需要进行服务迁移,业务方在升级了我们提供的中间件框架版本后,发现商品展示的创建时间跟预期不一致,整整提前了 13 个小时,而在升级前显示正常,如下图所示:


同一个商品升级前显示时间为 2021-03-31 16:52:21


升级后显示时间为 2021-04-01 05:52:21


故障修复

业务方确认只升级了中间件框架版本,没有做业务逻辑改动,因此问题归结到中间件相关版本可能引入的不兼容。由于影响到线上商品上货审查,因此第一时间让业务方回滚版本,故障恢复。


由于业务方要尽快完成服务迁移,因此必须尽快找出问题原因。因为涉及到数据库操作,所以第一时间确认了升级前后 mybatis 和 mysql-connector 的版本,发现升级前使用的是mysql-connector-java-5.1.47版本,而升级后通过 spring-boot-2.3.0 引入了mysql-connector-java-8.0.20版本,再进一步对比两个版本关于数据库时区的初始化配置,发现了差异,因此初步判断是 mysql-connector-java 版本引起的异常。


随后跟业务方讨论后决定在预发环境进行验证,仍然使用中间件新版本,但指定使用mysql-connector-java-5.1.47 版本。验证结果符合预期,商品时间显示恢复正常,业务方线上顺利完成迁移。

.

时差问题排查

虽然问题暂时得以解决,但是这个显示时间相差 13 个小时是怎么产生的,背后又有哪些逻辑,值得去探寻清楚。于是趁着周末在测试环境复现了场景,并梳理了整个流程,找到了相差 13 个小时的原因。也借此机会对时区的一些概念和 jdk 的实现逻辑有了更深的理解。

预备知识点 1 - CST 时区的理解

如果在中国,大多数人看到 CST 时区的第一反应就是中国标准时间(即 UTC+8),其实 CST 作为时区简写还可以表示:

Central Standard Time(USA,UTC-6)

Cuba Standard Time(Cuba,UTC-4)

Central Standard Time (Australia,UTC+09:30) 


在 JDK 中,根据 CST 查询出来的时区是 USA Central Standard Time(即 Ameraca/Chicago),如下图所示:

JDK中的CST时区(1)


JDK中的CST时区(2)


预备知识点 2 - 夏令时

夏令时(DST,Daylight Saving Time),是人为引入的在某一段时间范围内将时间统一往前加一个小时,在这一制度下实行的统一时间称为夏令时,全世界大约有一半的国家和地区实行夏令时。


美国的夏令时,从每年 3 月第 2 个星期天凌晨开始,到每年 11 月第 1 个星期天凌晨结束,即大约是每年的 3 月中旬到 11 月初。对美国中部时间 CST 来说,它属于UTC-6时区,在实行夏令时期间会往前调一个小时,相当于UTC-5时区。因此,北京时间(CST 即 UTC+8)跟芝加哥时间(美国中部时间 CST)正常情况相差 14 个小时,夏令时期间相差 13 个小时。

预备知识点 3 - 时间戳的理解

我们都知道 long 型时间戳表示的是当前时间与 UTC 零时区 1970 年 1 月 1 日零点的毫秒表示的时间差,因此在同一时间,任何时区的时间戳都是相同值。其实 long 型时间戳的参考点是 UTC 零时区,差值就是当前时间转换成 UTC 零时区时间后距离 1970-1-1 零点的毫秒数。下面的例子展示了在相同时间:

北京时间 2021-04-01 05:52:21(UTC+8)

芝加哥时间 2021-03-31 16:52:21 (UTC-6 夏令时)

GMT 时间 2021-03-31 21:52:21(UTC 零时区)

获取的时间戳在精确到秒时是相同的。

不同时区的同一时间点获取时间戳(1)


不同时区的同一时间点获取时间戳(2)


时差问题链路梳理

有了前面的预备知识做铺垫,下面将详细梳理 13 个小时的时差是如何产生的。

背景信息:

数据库时区设置为 CST

应用部署的服务器默认时区为 Asia/Shanghai


我们先来看下 mysql-connector-java 的 8.x 版本和 5.x 版本在初始化时区的差异,如下图所示:



数据库时区信息为SYSTEM,即系统默认时区


从上面的对比可以看出,红圈部分是处理的差异,在数据库连接串没有显示指定 serverTimezone 时,mysql-connector-java-8.0.20 版本直接使用从数据库获取的 CST 时区作为 session 时区,从前面得知,此时将使用的是 America/Chicago 时区,因此后续使用的都是这个 UTC-6 时区。

而 5.1.47 版本在红圈部分在默认配置下是不满足条件,因此在整个 configTimezone()并没有初始化 session 时区,因此后续用到的都是服务器默认时区,如下图所示:

mysql-connector-java-5.1.47使用默认时区获取字段值

其实从这里的逻辑差异就可以解释为什么原来使用 5.x 版本的 mysql-connector-java 能正常工作,而升级到 8.x 版本就展示异常。


下面在基于 mysql-connector-java-8.0.20 版本下,我们将串起查询时间字段的整个过程。

业务方使用 java.util.Date 类来表示 mysql 中的 timestamp 类型字段,在 8.x 版本中,字段值的解析由ValueFactory<T>类负责。在获取查询结果时,mysql-connetor 会先初始化结果集 ResultSet,其中就有对 timestamp 类型的SqlTimestampValueFactory的初始化,也能看到使用的时区是最开始获得的 serverTimezone(即 America/Chicago),如下图所示:

初始化ResultSet


SqlTimestampValueFactory中,我们可以看到这样的解释

Value factory to create Timestamp instances. Timestamp instances are created from fields returned from the db without a timezone. In order to create a point-in-time, a time zone must be provided to interpret the fields.


和具体的字段值解析逻辑:

// code from SqlTimestampValueFactory.javapublic Timestamp localCreateFromTimestamp(InternalTimestamp its) {        if (its.getYear() == 0 && its.getMonth() == 0 && its.getDay() == 0) {            throw new DataReadException(Messages.getString("ResultSet.InvalidZeroDate"));        }
synchronized (this.cal) { try { // this method is HUGEly faster than Java 8's Calendar.Builder() this.cal.set(its.getYear(), its.getMonth() - 1, its.getDay(), its.getHours(), its.getMinutes(), its.getSeconds()); Timestamp ts = new Timestamp(this.cal.getTimeInMillis()); ts.setNanos(its.getNanos()); return ts; } catch (IllegalArgumentException e) { throw ExceptionFactory.createException(WrongArgumentException.class, e.getMessage(), e); } } }
复制代码


这就是说,InternalTimestamp 表示的各个时间字段(从 DB 中解析出来的值)是不带时区标识的,比如 2021-03-31 16:52:21,它并不携带特定时区信息。在 America/Chicago 时区下,解析出的 Timestamp 应为 2021-03-31 16:52:21(UTC-6,此时 8 月份为夏令时),其对应的 UTC+0 时区时间应为 2021-03-31 21:52:21。


紧接着,那 java.sql.Timestamp 类型是如何变成了业务方最后使用的 java.util.Date 类型呢?

其实这里就是 mybatis 为我们做了映射,如下图所示:

mybatis注册各种类型和相应的类型处理器


在对应 java.util.Date 类型的 DateTypeHandler 中,我们可以看到 Timestamp 到 java.util.Date 的转换(见下图)


目前分析到这里,已经逐渐明朗,剩下的就是既然可以转换成 java.util.Date,那最后显示的为啥不是 UTC+0 时区的 2021-03-31 21:52:21,而是 UTC+8 的 2021-04-01 05:52:21 呢?

这里主要是因为 java.util.Date 类型就是以当前默认时区(-Duser.timezone 或者服务器系统时区)作为时间标识的依据,如下图所示:

java.util.Date以默认时区作为时间显示依据


所以最后得到的 java.util.Date 就是以当前服务器时区(Asia/Shanghai)的标识的时间,见下图:


至此,整个时差 13 个小时(如果是非夏令时,会相差 14 个小时)的问题得以水落石出,中间还是经过了很多转换。


总结思考

这个关于时区的坑,告诉我们不要迷信 CST 这种简写时区,因为重名的情况下会出现意想不到的 bug,最好使用 Asia/Shanghai 这样的 IANA 时区标识。另外,创建数据库时,最好也要显示指定时区,不要使用默认的系统时区(比如 CST)。

大家有遇到类似时区问题导致的异常么,欢迎留言探讨~

发布于: 18 分钟前阅读数: 4
用户头像

小江

关注

~做一个安静的码男子~ 2019.02.11 加入

软件工程师,目前在电商公司做研发效能平台,中间件维护开发相关工作

评论

发布
暂无评论
升级mysql-connector-java-8.x踩坑纪实