• 【曹工杂谈】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个小时。

    Copy
    <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,举例:

      Copy
      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版本不需要我们自己再去写这种代码:

      Copy
      // 注册 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过程#

    看看我们前面的代码,设置时间参数主要是下面这一行:

    Copy
    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,什么时候设置的#

    第一次设置(初始化)#

    Copy
    targetCalendar != null ? null : this.session.getServerSession().getDefaultTimeZone()

    这里面其实是获取了:

    Copy
    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:

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

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

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

    Copy
    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次挥手

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

    总结#

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




  • 相关阅读:
    一键自动化博客发布工具,用过的人都说好(segmentfault篇)
    世界前沿技术发展报告2023《世界信息技术发展报告》(四)电子信息技术
    云计算的openStack 究竟是为了解决什么问题?一句话说清楚
    PTA:L1-002 打印沙漏
    C++ 多级继承与多重继承:代码组织与灵活性的平衡
    【测试沉思录】8. 测试计划应该怎么做?
    果断收藏!考完PMP还能学什么?一文解答你的疑惑
    书生·浦语大模型开源体系(七)作业
    Web学习笔记-React(Redux)
    wx.request请求eggjs报invalid csrf token
  • 原文地址:https://www.cnblogs.com/grey-wolf/p/15915807.html