【曹工杂谈】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,可以看到,没有漏洞字样:

版本差异

  1. 先给一份官方的:

    其实可以看出来,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

  2. 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

  3. 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,也没影响。

  4. 还有些大家不用感知的,比如一些接口的包名发生变化,一些异常类被删除了,因为我们一般不会直接用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,开始了第二次设置。

这个方法比较长,我分两三段来截图。

上图比较清楚,就是:

  1. 获取服务端的"time_zone"配置,如果“time_zone”为“system”,则获取“system_time_zone”的配置

    我这边数据库吧,反正默认装好就是这样的,正好就是cst和system,也没动过,所以这也是为啥国内大家很多人遇到这个问题的原因。

  2. 获取客户端自身建立连接时候的配置,通俗来说,就是dbUrl里面那些connection property

  3. 如果客户端没配,则以服务端的为准

再接下来,就是以CST来设置成本次会话的默认时区。下面最后一行红框的,也就是这第二次设置。

解决问题的思路

通过上面,我们知道了,如果客户端没设置时区,就会用服务端的。所以,两种改法:

  1. 把服务端配置的system_time_zone和time_zone改成正确的,网上也有些教程,就是这样。但是我们这边公司大,数据库很多业务在用,这么改,怕影响到别人

  2. 客户端连接url中,指定时区

    也就是这样指定serverTimezone:

    jdbc:mysql://1.1.1.1:3306/test_ckl?useSSL=false&serverTimezone=Asia/Shanghai
    

我们改了客户端,再看看。

跑完程序,正常查询到数据:

id: 8; name:yyyy; time:22:49:27 

扩展信息

这个整个交互中,一共有如下几次网络请求。

  1. tcp三次握手
  2. 登录请求,带着用户名、密码去登录
  3. 接下来,就是那次查询服务端各种配置参数的请求,包括time_zone等全局variable
  4. show warnings,这次请求应该就是看看服务端有没有什么警告信息
  5. 客户端发起的,"set names latin1"
  6. 客户端发起:“SET character_set_results = NULL”
  7. 客户端发起:SET autocommit=1
  8. 我们的业务查询请求
  9. 结束会话
  10. 4次挥手

具体可以看下面的红框部分:

总结

这个参数在服务端的配置我还没来得及去看,不过对客户端的影响,基本大致了解了。如果对大家也有些帮助,荣幸之至,谢谢大家。

posted @ 2022-02-21 11:10  三国梦回  阅读(480)  评论(0编辑  收藏  举报