
ShardingSphere Pipeline 兼容 MySQL 时间类型 解读
背景
ShardingSphere
在日常中,开发者经常会用到时间类型,如果熟悉的时间类型涉及到了时区、精度,哪种时间类型更合适?JDBC 驱动中又有哪些注意事项?因此,对数据库时间类型有更深入的了解,有助于开发者对于相关问题的排查。
本文简要介绍常用的 MySQL 时间类型,并结合 ShardingSphere Pipeline 中的逻辑,解读在程序中如何兼容 MySQL 的时间类型。
MySQL 时间类型简介
ShardingSphere
MySQL 时间类型主要有如下 5 种
时间类型 | 所需空间(5.6.4前) | 所需空间(5.6.4后) | "0" 值 |
YEAR | 1 byte | 1 byte | 0 |
DATE | 3 bytes | 3 bytes | '0000-00-00' |
TIME | 3 bytes | 3 bytes + fractional seconds storage | '00:00:00' |
DATETIME | 8 bytes | 5 bytes + fractional seconds storage | '0000-00-00 00:00:00' |
TIMESTAMP | 4 bytes | 4 bytes + fractional seconds storage | '0000-00-00 00:00:00' |
TIME,DATETIME,TIMESTAMP 的存储结构在 MySQL 版本 5.6.4前后发生了变化,在5.6.4之后这 3 种类型都支持小数(Fractional Seconds Precision),可以给出0到6范围内的可选fsp值,以指定小数秒精度。值为 0 表示不存在小数部分。如果省略,则默认精度为0。
01 服务器时区的影响
这里的时区,指的是 MySQL 中的
- time_zone
配置 ,默认值是 SYSTEM,此时使用的是全局参数 system_time_zone 的值(默认是当前系统的时区),可以在 session 级别覆盖。
展示受时区影响的只有 TIMESTAMP,其他类型都不受时区影响,这里解释下 DATETIME 和 TIME 的底层数据结构,便于理解为什么不受时区影响。
DATETIME 的数据结构如下(5.6.4之后的版本)
底层是按照年月日时分秒存储的,这些数据在存取时未进行时区转换,同理,TIME 和 DATE 也是相似的。
而 TIMESTAMP 类型存入数据库的实际是 UTC 的时间,查询显示时会根据具体的时区显示不同的时间。
测试用例如下:
通过 MySQL 客户端连上 MySQL 服务端
通过 SET SESSION time_zone = "+8:00"; 修改了时区,timestamp 类型的字段查询结果会发生变化,其他的类型则都不会发生变化。
02 特殊的 "0" 值
MySQL 允许存储 "0" 值,有时候也不一定是 "0" 值,例如 date 类型,MySQL 也可以将不存在的日期值转为 "0" 值, 比如 2023-02-30。
不过这种行为有一个条件,sql_mode不能开启严格模式,即不能包含 STRICT_TRANS_TABLES or STRICT_ALL_TABLES。关于 sql_mode 的内容还有很多,这里就不展开了。
详情可参考官方文档 :
https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html
有些细节需要注意,MySQL 之前的版本支持 YEAR 类型指定长度,比如 YEAR(2) 和 YEAR(4),但是在 MySQL 8 最新的版本已经不允许指定 YEAR 的长度了。并且 MySQL 8 默认是使用严格模式的。
严格模式有助于保证数据的完整性,便于数据在不同环境,不同数据库系统中流转。
Pipeline 中对于时间类型的兼容
ShardingSphere
数据迁移,数据一致性校验,CDC 都会依赖 Pipeline 的底层功能,要求保证数据的正确性和完整性。由于时间类型会涉及到时区、精度,这部分相对来会有更多的地方需要兼容处理。
这里比较核心的就是 JDBC 驱动了,程序和数据库都是通过 JDBC 进行交互的,所以 JDBC 的一些参数配置,会影响到数据的展示。
在 Java 程序里,需要注意 Application 到 JDBC 这层的数据转换,了解时间类型在这步是如何处理的,在下文会通过一些例子来说明。MySQL 官方的 JDBC 驱动是 MySQL Connector/J ,使用 C/J 缩写来表示。
以下所有的例子,MySQL 服务器的时区默认都是 UTC 时区,如下
01 时区的注意事项
通过设置 C/J 连接属性 serverTimezone,可以覆盖服务器的时区。如果不指定,服务器配置的默认时区会生效。
最好手动指定 serverTimezone,避免出现 MySQL 中的默认时区和 Java 时区含义不一致的情况,比如 CST, 在 Java 的 TimeZone 中指的是 America/Chicago,但是在 MySQL 中却是China Standard Time。
参考:
https://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html
serverTimezone 的值,正常情况都需要和服务器的时区保持一致,否则可能会导致写入数据不符合预期。
这里使用 C/J 8 测试下,版本是 8.0.23
下面是测试用例
数据库的数据如下,其中 10,11 两条记录是新插入的数据。
其中 10 这条数据的 c_timestamp 和 c_datetime 已经发生数据的偏移,而 11 这条数据是正确的,可以通过下面命令验证
11 中的时间已经比程序中插入时间多了 8 小时。
通过结果我们来分析下原因,当 JDBC URL 中没有指定 serverTimezone 的时候发生了什么。在一开始获取 Connection 时,com.mysql.cj.protocol.a.NativeProtocol#configureTimeZone 中设置了 session 的 timezone。
connectionTimezone 等同于 serverTimezone ,执行是走了 if 逻辑中的第一个判断,也就是上图的 1。所以这个时候的 session timezone 已经变成了 Asia/Shanghai。
接下来看下 preparedStatement 设置的 java.sql.Timestamp 参数,JDBC 驱动是如何进行转换的,相关代码在 com.mysql.cj.ClientPreparedQueryBindings#bindTimestamp,如下
这里生效的逻辑是上图红色圈起来的部分,也就是 session timezone,上面确认过,是 Asia/Shanghai。所以这时 SimpleDateFormat 和 Timestamp 时区是一致的,format 结果就是 2023-07-07 12:01:00。
当 JDBC URL 中带有 serverTimezone时,此时 SimpleDateFormat 和 Timestamp 的时区不一致,format 结果就发生了时区转换。这种时区转换,结果是正确的。
但是回头看看最开头的那句话,和这个现象是违背的
通过设置 C/J 连接属性 serverTimezone,可以覆盖服务器的时区。如果不指定,服务器配置的默认时区会生效。
从结果来说,如果不设置 serverTimezone,MySQL 并不会使用服务器的时区,用的是 C/J 程序默认所在时区。这个现象,可以追溯到 https://bugs.mysql.com/bug.php?id=85816。
这里面相关的内容提到,C/J 8 为了保持和历史版本的行为一致,默认在 C/J 5 中,如果 JDBC URL 中没有配置 serverTimezone,用的就是 C/J 程序运行时所在的时区,事实上,C/J 5 只配置 serverTimezone 参数的话, 和 C/J 8 的行为仍然是不一致的。
即如果需要开启时区调整
- C/J 5.1 需要同时配置 useLegacyDatetimeCode=false&serverTimezone=XXX这两个参数
- C/J 8 只需要配置 serverTimezone=XXX
除了使用 Timestamp,也可以使用字符串形式写入时间类型,这种方式不会触发时区转换,感兴趣的同学可以自行 Debug 源码里面看一下。
对应的结果,可以看到使用 setString() 方法设置的时间是不会被调整的。
关于时区,除了关键的 serverTimezone 参数,还有一些另外的参数也会影响时区转换的逻辑,比如 C/J 8 中的 preserveInstants 和 forceConnectionTimeZoneToSession,可以参考官网的解释,这里就不展开了。https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-time-instants.html。如果出现时间不符合预期的情况,可以通过类似的方式进行排查。
02 精度的注意事项
MySQL 时间是有小数位的,最小精度是微秒,不过有以下几个要求
1、建表的时候需要指定小数位,不指定默认小数位是 0,也就说最小精度就是秒,将建表语句改写下。
测试用例如下
查看插入的数据,结果如下
- c_timestamp 和 c_datetime 的值都包含小数位,最后一位发生了四舍五入,所以小数位变成了 .123457。
- c_time 并没有小数位,虽然我们在代码中明确指定了。
改成使用 MySQL Client 插入类似的数据,发现可以正常写入小数位。所以这一定又和 C/J 的实现有关。
MySQL 官网中提到:根据 JDBC 的规范,对于 java.sql.Time 类型,设计上不应该包含小数,但是因为 java.sql.Time 是 java.sql.Date 的子类,实际使用中会包含小数信息,所以在高版本的驱动中其实是支持了小数,并且默认是打开的。在 8.0.23 版本中引入了一个连接属性,sendFractionalSecondsForTime,默认值就是 true。
从 8.0.23 开始 ,JDBC URL 中sendFractionalSeconds 的配置是全局控制所有时间类型发送小数点的。如果 sendFractionalSeconds=false,无论
sendFractionalSecondsForTime 的值如何,都不会发送小数秒。
我们用的就是 C/J 8,并且版本也是 8.0.23,但是为什么结果是错的,其实是 Java 代码中 LocalTime 和 Time 转换的行为导致的,Time.valueOf 方法会舍去 LocalTime 中的纳秒值,所以应该使用注释掉的那行方法。
但是如果使用的是低版本的驱动,比如 C/J 5,不论怎样都无法将小数秒传递到 MySQL 服务器。
使用 C/J 5.1 的测试下
JDBC42Helper.convertJavaTimeToJavaSql 的方法如下,对于 LocalTime,在这里使用的就是会丢失纳秒 Time.valueOf 方法。
看完了写入,再来看看读取,我们上面通过 MySQL Client 写入了一条 id = 2 的数据,通过 C/J 查询这条数据。
结果如下,数据库中含有的小数位也被丢弃了。
03 关于数据一致性对比
Pipeline 中的数据一致性校验模块被用于比较迁移前后的数据是否一致。这里也有一些问题需要注意
- 时区问题(上面已经讨论过,这里不重复了)
- 数据库字段有特殊值的问题(比如上面提到的 "0" 值)
- 数据库同一类型的字段在程序中展现的类型不一致(通常和 JDBC 驱动参数有关)
第二个问题,虽然数据库层面可以存储 "0" 值,但是通过 C/J 查询数据的时候,收到 zeroDateTimeBehavior参数的影响,这个参数 C/J 5 和 8 之间是不兼容的,仅在写法上有区别,具体含义没有发生变化。
- C/J 5:exception,round,convertToNull
- C/J 8:EXCEPTION,ROUND, CONVERT_TO_NULL
其中 exception 是默认值,通过下面的测试用例来验证下。
先插入一条数据
通过 JDBC 查询数据
得到一个异常
我们通过 C/J 8 中的一段源码来看下这个转换逻辑
默认的话会走到 localCreateFromTimestamp 方法, "0" 值的话会在下面这里抛出异常。
如果 C/J 中增加 CONVERT_TO_NULL 参数,则会返回 null,如果是 round 参数,则返回 0001-01-01 00:00:00,这些情况会导致源端和目标端两者的数据不一致。
除此之外,yearIsDateType也会影响 YEAR 的返回类型,如果为 true 则程序中读到的是 java.sql.Date 类型,否则就是 Short 类型。
结论
ShardingSphere
根据上面的一些结果和原因分析,可以得出一些通用的结论,可以尽量避免时间类型使用中出现的问题。
1、首先 MySQL time_zone 不推荐使用 SYSTEM,官网原文如下
If set to SYSTEM, every MySQL function call that requires a time zone calculation makes a system library call to determine the current system time zone. This call may be protected by a global mutex, resulting in contention.
2、尽量使用高版本的驱动,支持的功能更多,官方文档也更详细。
3、最好保证应用程序和数据库之间时区是一样的,如果真的有跨时区展示的需求,考虑是否可以用整型代替 MySQL 中的 timestamp 和 datetime 类型。
以上就是本次分享的全部内容,如果读者对 Apache ShardingSphere 有任何疑问或建议,欢迎在 GitHub issue 列表提出。
文章转载自公众号:ShardingSphere官微
