mysql-connector-java8 时间字段保存错误排查(一)

背景

最近新开一个项目,创建工程时为了与时俱进,springboot使用了2.3.3.RELEASE,orm框架使用mybatis,mybatis-spring-boot-starter相应的使用了2.1.3,mybatis generator使用了1.4.0。
然后就是巴拉巴拉码代码(顺便吐槽下mabatis-generator使用MyBatis3DynamicSql方式生成的代码,使用起来跟jooq还真特么像啊哈哈。)
好了,代码码好开始自测了,结果发现问题了

  1. 保存记录的时候,mysql表的timestamp字段保存的值不对,数据库保存的值比实际时间慢13小时。
  2. 查询记录的时候,查出的结果比数据库保存的值又快了5个小时。
    把mybatis的sql打印出来,发现保存记录的时候传给mysql的时间是对的。这就奇怪了,而且更奇怪的是,13个小时和5个小时这是什么鬼,跟UTC时间也扯不上边啊。
解决过程
  1. 寻找差异
    1. 最初怀疑是我使用的开发库是不是时区设置的不对
      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b8qQ2FX8-1600052839667)(http://pfp.ps.netease.com/kmspvt/file/5f5c728b68d8646962dbc50cZr953YvB01?sign=UqEjuXI1d5xySGlyBru6Iv8IwAs=&expire=1600054416)]
      跟测试库和其他工程开发库对比了一下,发现没差别。所以问题不是出自这里。但是还是有个疑惑啊,什么是CST时区呢。
      搜索引擎了一下
    CST (时区缩写) 编辑
    CST可视为美国、澳大利亚、古巴或中国的标准时间.
    美国中部时间:Central Standard Time (USA) UT-6:00
    澳大利亚中部时间:Central Standard Time (Australia) UT+9:30
    中国标准时间:China Standard Time UT+8:00
    古巴标准时间:Cuba Standard Time UT-4:00
    
    什么鬼,居然CST可以视为四个时区的时间,这么说假设CST使用了美国中部时间的话(为啥不假设用古巴标准时间呢?美帝开发的软件,可能会精古吗,直接排除。。。),那咱是UT+8,它是UT-6,那存到库里的时间应该是中国时间-14,这虽然接近13,但是毕竟不是啊,刚以为看到了曙光,结果又被打击了。无论如何这是个线索啊,google一下吧。
    The North American Central Time Zone (CT) is a time zone in parts of Canada, the United States, Mexico, Central America, some Caribbean Islands, and part of the Eastern Pacific Ocean.
    Central Standard Time (CST) is six hours behind Coordinated Universal Time (UTC). During summer most of the zone uses daylight saving time (DST), and changes to Central Daylight Time (CDT) which is five hours behind UTC.[1]
    
    daylight saving time是啥啊,原谅我的无知,继续搜索引擎吧
    夏令时,表示为了节约能源,人为规定时间的意思。也叫夏时制,夏时令(Daylight Saving Time:DST),又称“日光节约时制”和“夏令时间”,在这一制度实行期间所采用的统一时间称为“夏令时间”。
    
    必须承认,看到这个结果的时候我的心里真是五味杂陈,啊,夏令时,一个埋藏在记忆深处的名词,一个多么暴露年龄的名词啊。原来mysql给我保存时间的时候,不仅用了美帝的时区,还用了美帝的夏令时。。。
    可是可是,为什么之前的其他工程使用springboot1.5.9没出现过这种问题呢。联想到mysql-connector-java包升级到了8.0.15,连spring.datasource.driverClassName都跟以前不一样,变成com.mysql.cj.jdbc.Driver了,那是不是时区设置方面,也发生了某些不兼容的变化呢。。。
  2. 翻文档
    既然怀疑是mysql-connector-java包升级导致的问题,那就去翻翻mysql 8的文档吧,看看有没有什么跟时区有关的内容。结果找到这么一段
      Connector/J 8.0 always performs time offset adjustments on date-time values, and the adjustments require one of the following to be true:
    
      The MySQL server is configured with a canonical time zone that is recognizable by Java (for example, Europe/Paris, Etc/GMT-5, UTC, etc.)
    
      The server's time zone is overridden by setting the Connector/J connection property serverTimezone (for example, serverTimezone=Europe/Paris).
    
    看到这里大概的意思是想要时区正常,您要么修改mysql server的配置,要么您就要在连接url后边加上正确的serverTimezone。wtf,为啥呢,为啥呢。那反正我是没权限改服务器配置的,只能选择后者呢。然而还是想问,为啥呢。没其他的办法,看代码吧

3.看代码
下面是5.1.46的代码

     private void configureTimezone() throws SQLException {
        String configuredTimeZoneOnServer = this.serverVariables.get("timezone");

        if (configuredTimeZoneOnServer == null) {
            configuredTimeZoneOnServer = this.serverVariables.get("time_zone");

            if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
                configuredTimeZoneOnServer = this.serverVariables.get("system_time_zone");
            }
        }

        String canonicalTimezone = getServerTimezone();

        if ((getUseTimezone() || !getUseLegacyDatetimeCode()) && configuredTimeZoneOnServer != null) {
            // user can override this with driver properties, so don't detect if that's the case
            if (canonicalTimezone == null || StringUtils.isEmptyOrWhitespaceOnly(canonicalTimezone)) {
                try {
                    canonicalTimezone = TimeUtil.getCanonicalTimezone(configuredTimeZoneOnServer, getExceptionInterceptor());
                } catch (IllegalArgumentException iae) {
                    throw SQLError.createSQLException(iae.getMessage(), SQLError.SQL_STATE_GENERAL_ERROR, getExceptionInterceptor());
                }
            }
        }
         if (canonicalTimezone != null && canonicalTimezone.length() > 0) {
            this.serverTimezoneTZ = TimeZone.getTimeZone(canonicalTimezone);

下面是8.0.15的

 public void configureTimezone() {
        String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone");

        if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
            configuredTimeZoneOnServer = this.serverSession.getServerVariable("system_time_zone");
        }

        String canonicalTimezone = getPropertySet().getStringProperty(PropertyKey.serverTimezone).getValue();

        if (configuredTimeZoneOnServer != null) {
            // user can override this with driver properties, so don't detect if that's the case
            if (canonicalTimezone == null || StringUtils.isEmptyOrWhitespaceOnly(canonicalTimezone)) {
                try {
                    canonicalTimezone = TimeUtil.getCanonicalTimezone(configuredTimeZoneOnServer, getExceptionInterceptor());
                } catch (IllegalArgumentException iae) {
                    throw ExceptionFactory.createException(WrongArgumentException.class, iae.getMessage(), getExceptionInterceptor());
                }
            }
        }
        if (canonicalTimezone != null && canonicalTimezone.length() > 0) {
            this.serverSession.setServerTimeZone(TimeZone.getTimeZone(canonicalTimezone));

看起来注册时区的逻辑还是差不多的,先取服务器参数time_zone,如果是SYSTEM,就取服务器参数system_time_zone;然后如果url里没有指定serverTimeZone,就拿system_time_zone作为时区了,所以说两个版本里最终取到的时区都是CST。wtf,那就继续看看保存数据的代码吧。
5.1.46的,代码取自PreparedStatement、ConnectionImpl、TimeUtil

 public void setTimestamp(int parameterIndex, Timestamp x) throws java.sql.SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            setTimestampInternal(parameterIndex, x, null, this.connection.getDefaultTimeZone(), false);
        }
    }

跟着this.connection.getDefaultTimeZone()一直往上走,最终

    private static final TimeZone DEFAULT_TIMEZONE = TimeZone.getDefault();

哦,原来5.1.46里面保存timestamp字段时,时区是直接取了系统的时区。
那再看8.0.15的,代码来自PreparedStatementWrapper,ClientPreparedQueryBindings

 @Override
    public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
        try {
            if (this.wrappedStmt != null) {
                ((PreparedStatement) this.wrappedStmt).setTimestamp(parameterIndex, x);
            } else {
                throw SQLError.createSQLException(Messages.getString("Statement.AlreadyClosed"), MysqlErrorNumbers.SQL_STATE_GENERAL_ERROR,
                        this.exceptionInterceptor);
            }
        } catch (SQLException sqlEx) {
            checkAndFireConnectionError(sqlEx);
        }
    }
 @Override
    public void setTimestamp(int parameterIndex, Timestamp x, Calendar targetCalendar, int fractionalLength) {
            this.tsdf = TimeUtil.getSimpleDateFormat(this.tsdf, "''yyyy-MM-dd HH:mm:ss", targetCalendar,
                    targetCalendar != null ? null : this.session.getServerSession().getDefaultTimeZone());
                    
    /** c.f. getDefaultTimeZone(). this value may be overridden during connection initialization */
    private TimeZone defaultTimeZone = TimeZone.getDefault();

咦,这里也是TimeZone.getDefault(); ,我*&¥#&*#,别激动别激动,看看注释,再回去看看注册时区的代码,原来是defaultTimeZone在那里被修改了;好了,现在知道了,8.0.15保存timestamp字段的时候是使用了mysql或者用户配置的时区。
然而,难道5.1.46就不用mysql或者用户配置的时区吗,那不是大bug吗,好吧回去继续看代码,代码取自PreparedStatement、TimeUtil

private void setTimestampInternal(int parameterIndex, Timestamp x, Calendar targetCalendar, TimeZone tz, boolean rollForward) throws SQLException {
            if (!this.useLegacyDatetimeCode) {
                newSetTimestampInternal(parameterIndex, x, targetCalendar);
            } else {
                Calendar sessionCalendar = this.connection.getUseJDBCCompliantTimezoneShift() ? this.connection.getUtcCalendar()
                        : getCalendarInstanceForSessionOrNew();

                x = TimeUtil.changeTimezone(this.connection, sessionCalendar, targetCalendar, x, tz, this.connection.getServerTimezoneTZ(), rollForward);

原来setTimestampInternal里还有玄机,当useLegacyDatetimeCode是true的时候(默认为true),会修改时区,看到this.connection.getServerTimezoneTZ()没,这就是前面注册时区的时候获取的mysql服务器或者url里配置的时区

public static Timestamp changeTimezone(MySQLConnection conn, Calendar sessionCalendar, Calendar targetCalendar, Timestamp tstamp, TimeZone fromTz,
            TimeZone toTz, boolean rollForward) {
        if ((conn != null)) {
            if (conn.getUseTimezone()) {

细节就不管了,总之useTimeZone为true时,就按照时区toTz来修正时间,嗯,结论就是如果你在url里配置上useTimeZone=true,5.1.46也也一样可以给你保存成美帝夏令时。。。。
代码研究完了,那在url里加上serverTimezone=Asia/Shanghai吧。来,自测一下,保存的时间对了。好!很有精神!
那再顺便测一下查询记录吧
。。。。。。

。。。。。。

。。。。。。
纳尼,什么,WHAT,获取到的时间比数据库保存时间慢了8小时,哈哈哈哈,我差点都忘了,还有问题2没有解决呢。

欲知后事如何,请听下回分解。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: mysql-connector-javamysql-connector-j是同一个MySQL官方提供的JDBC驱动程序,它用于连接MySQL数据库和Java应用程序。mysql-connector-javaMySQL Connector/J的完整名称,其中“J”表示Java,是指这是一个Java驱动程序。而mysql-connector-j则是mysql-connector-java的简写,两者是同一个东西,只是名称不同。 ### 回答2: mysql-connector-javamysql-connector-j都是用于连接Java应用程序和MySQL数据库的驱动程序。它们之间的主要区别可以从以下几个方面来看。 1. 命名方式:mysql-connector-java是根据JDBC(Java数据库连接)的命名方式来命名的,而mysql-connector-j是MySQL Connector/J的缩写命名方式。 2. 版本历史:mysql-connector-javaMySQL官方发布的Java驱动程序,它的版本号与MySQL数据库的版本号是对应的。而mysql-connector-j是在mysql-connector-java的基础上进行二次开发和维护的版本。 3. 社区贡献:mysql-connector-javaMySQL官方维护,更新频率较稳定,并提供了常见的功能和支持。而mysql-connector-j则是由独立的开发者或第三方贡献者维护,更新可能相对较少。 4. 功能支持:由于mysql-connector-javaMySQL官方的驱动程序,它相对完整地支持了MySQL数据库的各种功能,如事务处理、存储过程等。而mysql-connector-j则可能只支持部分数据库功能,具体取决于开发者对其进行的二次开发。 总体来说,mysql-connector-java是更常用和可靠的MySQL数据库驱动程序,由MySQL官方提供支持和维护。而mysql-connector-j则可能是由第三方进行了一些个性化的开发和定制,可能用于特定的应用场景。选择使用哪个驱动程序取决于具体的需求和项目要求。 ### 回答3: mysql-connector-javamysql-connector-j其实是指的同一个东西,都是用于Java程序连接MySQL数据库的驱动程序。mysql-connector-java是该驱动程序的官方名称,而mysql-connector-j则是该驱动程序的简称。 mysql-connector-javaJava语言开发的,它提供了一个API,使得Java程序可以直接连接和操作MySQL数据库。通过mysql-connector-javaJava程序可以执行数据的增删改查操作,执行SQL语句,以及连接和断开数据库等。 mysql-connector-j是mysql-connector-java的缩写形式,常用于命令行或脚本的写作。在一些场景中,为了方便输入或提高效率,人们更倾向于使用mysql-connector-j这个简称。 总结来说,mysql-connector-javamysql-connector-j在功能和使用上并没有实质的差别,只是一个是官方名称,一个是简称。无论是使用mysql-connector-java还是mysql-connector-j,都是为了实现Java程序与MySQL数据库的连接和操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值