【曹工杂谈】Mysql-Connector-Java时区问题的一点理解--写入数据库的时间总是晚13小时问题
背景
去年写了一篇“【曹工杂谈】Mysql客户端上,时间为啥和本地差了整整13个小时,就离谱 ”,结果最近还真就用上了。
不是我用上,是组内一位同事,他也是这样:有个服务往数据库insert记录,记录里有时间,比如时间A。然后写进数据库后,数据库里的时间是A-13,晚了13小时。然后就改了这么个地方:
写进去的数据,就是正确的时间了。
后边,他还有一个查询服务,要去查写进去那条记录,比如记录有个创建时间字段,字段值是2022-02-19 00:00:00. 然后假设我查的时候,就根据这个时间来查,传个2022-02-19 00:00:00。结果发现,查不到。为啥呢,因为参数里的时间也被减了13个小时,导致和服务器端记录的时间匹配不上了。
其实,两个问题,是同一个问题,最终的解决办法也是一样的。
这个问题,抽象一下,就是,在mysql-connector-java 8.0.x版本下,我们发送给服务器的时间,为啥会少了13个小时。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
关于mysql-connector-java
主要版本
现在主流的版本,有两个,5.1.x系列和8.0.x系列,5.1.x系列最新的一个版本是5.1.49.
大家看下图,有红色字样的 "1 vulnerability",表示有漏洞,这也是为什么我们同事为啥要升级或者是被安全组逼着升级到8.0.x版本的原因。
8.0.x的最新版本是8.0.28,可以看到,没有漏洞字样:
版本差异
-
先给一份官方的:
其实可以看出来,5.1和8.0的兼容性都不错,都支持mysql server端:5.6/5.7/8/0,差异无非是对jre和jdk的版本不一样。
这里多说一句,mysql-connector-java是jdbc规范的一个实现,jdbc规范相关接口(java.sql和javax.sql里的就是,比如java.sql.Driver),跟随jdk一起发布。
jdbc规范版本 jdk 4.0 jdk 6 4.1 jdk 7 4.2 jdk 8 4.3 jdk 9及以后 可参考:https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/java/sql/package-summary.html
-
connection property发生了变化,什么是connection property,举例:
jdbc:mysql://1.1.1.1:3306/test?useSSL=false&serverTimezone=Asia/Shanghai
上面的useSSL、serverTimezone就是connection property。
具体变化:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-properties-changed.html
-
mysql driver的类名也发生了变化,5.1.x版本是叫 "com.mysql.jdbc.Driver",8.0.x里面是 "com.mysql.cj.jdbc.Driver",而且,8.0版本不需要我们自己再去写这种代码:
// 注册 JDBC 驱动 String JDBC_DRIVER = "com.mysql.jdbc.Driver"; Class.forName(JDBC_DRIVER);
当然了,8.0版本对5.1版本做了兼容,你即使加载5.1的driver,也没影响。
-
还有些大家不用感知的,比如一些接口的包名发生变化,一些异常类被删除了,因为我们一般不会直接用mysql-connector-java去编程,我们都是用jdbc接口嘛,实现类再怎么变,也没什么影响
https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-exceptions-changes.html
错误的时间,是客户端发送前就错了,还是服务端错了
界定问题范围
问一下自己这个问题,主要是界定问题发生的地方。这个也容易界定,最理想的方式就是网络抓包,wireshark或者tcpdump自己选吧。
这里先看下我的测试程序要做的事:
数据库有下面这一条记录,我要做的,就是根据时间参数,把记录查出来。
程序如下:
我如果实际执行这个demo,是查不出结果的,为啥呢,我网络抓包的截图给大家看看:
至于这个错误的时间,是怎么来的,那可能确实需要慢慢去debug。
debug过程
看看我们前面的代码,设置时间参数主要是下面这一行:
Timestamp timestamp = new Timestamp(simpleDateFormat.parse("2022-02-17 22:49:27").getTime());
preparedStatement.setTimestamp(1, timestamp);
那我们直接一点,就在这行打上断点,开始调试:
这里看得出来,是给this.query这个对象,设置相关的绑定参数。我们继续跟进:
此时,时间依然还是正确的。我们传了4个参数到setTimestamp方法,注意,第三个参数targetCalendar为null,这个参数会影响内部的分支。
看上图,这里因为targetCalendar为null,所以会去获取当前这个mysql会话中的时区字段。
这个时区是啥呢,就是CST。
也就是说,2022-02-17 22:49:27 这个时间,在CST时区下,就是 2022-02-17 08:49:27。
这里CST说是有好几个时区都是这个缩写,比如:
- Central Standard Time, North America's Central Time Zone: UTC−06:00,这个时间基本就是北美中部时间,北美中部包括了:美国、加拿大、墨西哥的中部地区
- China Standard Time: UTC+08:00,这个就是中国的北京时间了,但感觉CST一般还是指:北美中部时间
- Cuba Standard Time: UTC−04:00,这个其实点链接,会跳转进入美洲东部时间的wiki,因为古巴也是在北美东部位置,包括了:美国、加拿大、墨西哥东南、巴拿马、哥伦比亚、厄瓜多尔、秘鲁等(这里也有中美洲的一些地区)
可能国际上来说,看到CST,首先是任务是美国中部时区Central Standard Time(USA)UTC-06:00。一般不是是另外两个时区,中国那肯定就是Asia/Shanghai,古巴这种小国,存在感也较弱
这个时区,是零时区 - 6(美国冬令时,从11月7日到3月11日)或者是零时区 - 5(夏令时,从“3月11日”至“11月7日”),因为现在是美国的冬令时,所以这里差14小时(我们是东八区嘛,8 + 6)。
ok,言归正传,反正问题就是出现在:会话的时区不对,为啥是CST啊,能不能改?
会话中的时区变量,怎么是CST,什么时候设置的
第一次设置(初始化)
targetCalendar != null ? null : this.session.getServerSession().getDefaultTimeZone()
这里面其实是获取了:
com.mysql.cj.protocol.a.NativeServerSession#getDefaultTimeZone
private TimeZone defaultTimeZone = TimeZone.getDefault();
public TimeZone getDefaultTimeZone() {
return this.defaultTimeZone;
}
我们可以在这个字段上打个断点,看看这个值什么时候被设置:
然后重新debug整个程序,看看什么时候进入该field断点。我们会发现,第一次进入,就是在new这个类的对象时,
可以看看这个堆栈,基本就是获取connection的时候,相当于就是建立一个会话,所以这里会去new一个会话出来。
我看了下,在我机器上,初始化后,是东八区。
在第一次设置和第二次设置之间
这之间发生了一次重要的网络请求,
客户端向服务端请求各种服务端的variable,也就是服务端的配置。上面有两个时区相关的,system_time_zone和time_zone。
第二次设置
接下来,运行到了com.mysql.cj.protocol.a.NativeProtocol#configureTimezone
,开始了第二次设置。
这个方法比较长,我分两三段来截图。
上图比较清楚,就是:
-
获取服务端的"time_zone"配置,如果“time_zone”为“system”,则获取“system_time_zone”的配置
我这边数据库吧,反正默认装好就是这样的,正好就是cst和system,也没动过,所以这也是为啥国内大家很多人遇到这个问题的原因。
-
获取客户端自身建立连接时候的配置,通俗来说,就是dbUrl里面那些connection property
-
如果客户端没配,则以服务端的为准
再接下来,就是以CST来设置成本次会话的默认时区。下面最后一行红框的,也就是这第二次设置。
解决问题的思路
通过上面,我们知道了,如果客户端没设置时区,就会用服务端的。所以,两种改法:
-
把服务端配置的system_time_zone和time_zone改成正确的,网上也有些教程,就是这样。但是我们这边公司大,数据库很多业务在用,这么改,怕影响到别人
-
客户端连接url中,指定时区
也就是这样指定serverTimezone:
jdbc:mysql://1.1.1.1:3306/test_ckl?useSSL=false&serverTimezone=Asia/Shanghai
我们改了客户端,再看看。
跑完程序,正常查询到数据:
id: 8; name:yyyy; time:22:49:27
扩展信息
这个整个交互中,一共有如下几次网络请求。
- tcp三次握手
- 登录请求,带着用户名、密码去登录
- 接下来,就是那次查询服务端各种配置参数的请求,包括time_zone等全局variable
- show warnings,这次请求应该就是看看服务端有没有什么警告信息
- 客户端发起的,"set names latin1"
- 客户端发起:“SET character_set_results = NULL”
- 客户端发起:SET autocommit=1
- 我们的业务查询请求
- 结束会话
- 4次挥手
具体可以看下面的红框部分:
总结
这个参数在服务端的配置我还没来得及去看,不过对客户端的影响,基本大致了解了。如果对大家也有些帮助,荣幸之至,谢谢大家。