[转帖]mysql-connect-java驱动从5.x升级到8.x的CST时区问题
https://juejin.cn/post/7029291622537887774
前言
旧项目MySQL Java升级驱动,本来一切都好好的,但是升级到8.x的驱动后,发现入库的时间比实际时间相差13个小时,这就很奇怪了,如果相差8小时,那么还可以说是时区不对,从驱动源码分析看看
1. demo
pom依赖,构造一个真实案例,这里的8.0.22版本
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.5.4</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-api</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
随意写一个dao controller main
@SpringBootApplication
@MapperScan("com.feng.mysql.rep")
public class MySQLDateMain {
    public static void main(String[] args) {
        SpringApplication.run(MySQLDateMain.class, args);
    }
}
@RestController
public class UserController {
    @Autowired
    private UserRepository userRepository;
    @RequestMapping(value = "/Users/User", method = RequestMethod.POST)
    public String addUser(){
        UserEntity userEntity = new UserEntity();
        userEntity.setAge(12);
        userEntity.setName("tom");
        userEntity.setCreateDate(new Date(System.currentTimeMillis()));
        userEntity.setUpdateDate(new Timestamp(System.currentTimeMillis()));
        userRepository.insertUser(userEntity);
        return "ok";
    }
}
@Mapper
public interface UserRepository {
    @Insert("insert into User (name, age, createDate, updateDate) values (#{name}, #{age}, #{createDate}, #{updateDate})")
    int insertUser(UserEntity userEntity);
}
数据库设计
CREATE TABLE `work`.`User`  (
  `id` int(10) UNSIGNED ZEROFILL NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `age` int NULL DEFAULT NULL,
  `createDate` timestamp NULL DEFAULT NULL,
  `updateDate` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 29 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
1.1 验证
系统时间
调用http接口http://localhost:8080/Users/User
可以看到与真实时间相差13小时,诡异了,明明数据库时间是正确的, 而且我的系统时间也是正确的,那么我们可以就只能在驱动找原因,因为当我使用。
2.问题原因分析
2.1 时区获取
上一步,我们看见系统时间,数据库时间都是正确的,那么做文章的推断就是驱动造成的,以8.0.22驱动为例
使用的驱动是
com.mysql.cj.jdbc.Driver
当应用启动后,首次发起数据库操作,就会创建jdbc的代码,mybatis把这事情干了,获取连接,从连接池,笔者使用
HikariDataSource,HikariPool连接池
在com.mysql.cj.jdbc.ConnectionImpl里面会初始化 session的拦截器,属性Variables,列映射,自动提交信息等等,其中有一行代码初始化时区
kotlin复制代码this.session.getProtocol().initServerSession();
com.mysql.cj.protocol.a.NativeProtocol
    public void configureTimezone() {
        //获取MySQL server端的时区
        String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone");
        //如果是SYSTEM,则获取系统时区
        if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
            configuredTimeZoneOnServer = this.serverSession.getServerVariable("system_time_zone");
        }
        //配置文件获取时区serverTimezone配置,即可以手动配置,这是一个解决问题的手段
        String canonicalTimezone = getPropertySet().getStringProperty(PropertyKey.serverTimezone).getValue();
        //未指定时区,且读取到MySQL时区,就
        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));
            //
            // The Calendar class has the behavior of mapping unknown timezones to 'GMT' instead of throwing an exception, so we must check for this...
            //时区不规范,比如不是GMT,然而ID标识GMT
            if (!canonicalTimezone.equalsIgnoreCase("GMT") && this.serverSession.getServerTimeZone().getID().equals("GMT")) {
                throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Connection.9", new Object[] { canonicalTimezone }),
                        getExceptionInterceptor());
            }
        }
    }
规范时区
   /**
     * Returns the 'official' Java timezone name for the given timezone
     *
     * @param timezoneStr
     *            the 'common' timezone name
     * @param exceptionInterceptor
     *            exception interceptor
     *
     * @return the Java timezone name for the given timezone
     */
    public static String getCanonicalTimezone(String timezoneStr, ExceptionInterceptor exceptionInterceptor) {
        if (timezoneStr == null) {
            return null;
        }
        timezoneStr = timezoneStr.trim();
        // handle '+/-hh:mm' form ...
        //顾名思义
        if (timezoneStr.length() > 2) {
            if ((timezoneStr.charAt(0) == '+' || timezoneStr.charAt(0) == '-') && Character.isDigit(timezoneStr.charAt(1))) {
                return "GMT" + timezoneStr;
            }
        }
        synchronized (TimeUtil.class) {
            if (timeZoneMappings == null) {
                loadTimeZoneMappings(exceptionInterceptor);
            }
        }
        String canonicalTz;
        //时区缓存去找关键字
        if ((canonicalTz = timeZoneMappings.getProperty(timezoneStr)) != null) {
            return canonicalTz;
        }
        throw ExceptionFactory.createException(InvalidConnectionAttributeException.class,
                Messages.getString("TimeUtil.UnrecognizedTimezoneId", new Object[] { timezoneStr }), exceptionInterceptor);
    }
比如我的数据库时区是CST,拿到了
这是系统时区,拿到的是CST,根源是读取了内置的时区值
然而这个文件没有CST时区定义,需要去JDK去拿,然后缓存,这就说明一个道理CST这个时区定义不明确
时区就是CST了,仅仅是CST时区而已,这里并不能说明CST有什么问题,真正的问题是CST怎么比东八区少13个小时呢
this.serverSession.setServerTimeZone(
TimeZone.getTimeZone(canonicalTimezone));
TimeZone.getTimeZone(canonicalTimezone) //根源就是这几句代码
    public static TimeZone getTimeZone(String var0) {
        return ZoneInfoFile.getZoneInfo(var0);
    }
开始初始化,
sun.timezone.ids.oldmapping 这个一般不会设置
读取$JAVA_HOME/lib/tzdb.dat,这是一个JDK时区存储文件
其中PRC就是中国时区,但是这个文件并未定义CST
CST在这里定义的
addOldMapping();
    private static void addOldMapping() {
        String[][] var0 = oldMappings;
        int var1 = var0.length;
        for(int var2 = 0; var2 < var1; ++var2) {
            String[] var3 = var0[var2];
            //这里就把CST时区设置为芝加哥时区
            aliases.put(var3[0], var3[1]);
        }
        if (USE_OLDMAPPING) {
            aliases.put("EST", "America/New_York");
            aliases.put("MST", "America/Denver");
            aliases.put("HST", "Pacific/Honolulu");
        } else {
            zones.put("EST", new ZoneInfo("EST", -18000000));
            zones.put("MST", new ZoneInfo("MST", -25200000));
            zones.put("HST", new ZoneInfo("HST", -36000000));
        }
    }
oldMappings是啥呢
private static String[][] oldMappings = new String[][]{{"ACT", "Australia/Darwin"}, {"AET", "Australia/Sydney"}, {"AGT", "America/Argentina/Buenos_Aires"}, {"ART", "Africa/Cairo"}, {"AST", "America/Anchorage"}, {"BET", "America/Sao_Paulo"}, {"BST", "Asia/Dhaka"}, {"CAT", "Africa/Harare"}, {"CNT", "America/St_Johns"}, {"CST", "America/Chicago"} , {"CTT", "Asia/Shanghai"}, {"EAT", "Africa/Addis_Ababa"}, {"ECT", "Europe/Paris"}, {"IET", "America/Indiana/Indianapolis"}, {"IST", "Asia/Kolkata"}, {"JST", "Asia/Tokyo"}, {"MIT", "Pacific/Apia"}, {"NET", "Asia/Yerevan"}, {"NST", "Pacific/Auckland"}, {"PLT", "Asia/Karachi"}, {"PNT", "America/Phoenix"}, {"PRT", "America/Puerto_Rico"}, {"PST", "America/Los_Angeles"}, {"SST", "Pacific/Guadalcanal"}, {"VST", "Asia/Ho_Chi_Minh"}};
{"CST", "America/Chicago"}
    private static ZoneInfo getZoneInfo0(String var0) {
        try {
            //缓存获取
            ZoneInfo var1 = (ZoneInfo)zones.get(var0);
            if (var1 != null) {
                return var1;
            } else {
                String var2 = var0;
                if (aliases.containsKey(var0)) {
                    var2 = (String)aliases.get(var0);
                }
                int var3 = Arrays.binarySearch(regions, var2);
                if (var3 < 0) {
                    return null;
                } else {
                    byte[] var4 = ruleArray[indices[var3]];
                    DataInputStream var5 = new DataInputStream(new ByteArrayInputStream(var4));
                    var1 = getZoneInfo(var5, var2);
                    //首次获取,存缓存
                    zones.put(var0, var1);
                    return var1;
                }
            }
        } catch (Exception var6) {
            throw new RuntimeException("Invalid binary time-zone data: TZDB:" + var0 + ", version: " + versionId, var6);
        }
    }
就这样CST时区就被JDK认为是美国芝加哥的时区了,
2.2 时区设置
那么jdbc在哪里设置时间的呢
进一步可以看到在服务器上时区都是OK的
但是在com.mysql.cj.ClientPreparedQueryBindings的setTimestamp方法中,获取了session时区,然后format,
时间从此丢失13小时,原因是format的锅,因为用的美国芝加哥时间格式化,如果使用long时间的话或者什么都不处理就没有问题
SimpleDateFormat设置CST时区,前面已经分析了,这个时区就是美国芝加哥时区。
JDK会认为CST是美国芝加哥的时区,UTC-5,但是我们的时间是UTC+8,换算成US的时间就是,当前时间-8-5,即时间少13小时。这里不设置时区(即使用客户端时区)即可正常返回时间。
那么CST时区是什么呢,笔者写博客的时间是2021-09-22,是CST的夏令时
CST是中部标准时间,现在是UTC-5,即夏令时,冬季还会变成UTC-6
标准的US的CST时间是UTC-6,我当前的时间是23:56
关键在于CST定义非常模糊,而MySQL驱动调用SimpleDateFormat,使用的CST为美国芝加哥时区,当前的季节为UTC-5。
3.解决办法
根据上面的分析,解决CST时区的方法非常多
- 设置MySQL server的时区为非CST时区
- 设置MySQL的系统时区为非CST时区
- 通过参数增加serverTimezone设置为明确的MySQL驱动的properties定义的时区
- 修改MySQL Java驱动,获取时区通过客户端获取,比如当前运行环境,通过JDK获取
3.1 解决办法详细
设置MySQL server的时区
set global time_zone = ``'+08:00'``;
   或者修改MySQL的配置文件/etc/mysql/mysql.conf.d/mysqld.cnf  [mysqld]节点下增加
   default-time-zone = '+08:00'
设置系统时区,以Ubuntu为例
timedatectl set-timezone Asia/Shanghai
参数增加serverTimezone
   jdbc:mysql://localhost:3306/work?useUnicode=true&characterEncoding=utf-8&useSSL=true &serverTimezone=Asia/Shanghai
修改MySQL驱动
比如获取时区通过client端获取,Date数据使用什么时区,就使用这个时区format,但是一般而言我们不会自己发布驱动,跟随MySQL官方更新,只有大厂有机会自己运营MySQL驱动。
3.2 官方解决方案
笔者在浏览MySQL 8.0.x驱动发布的时候在8.0.23版本发现了特别的发布记录,笔者在初始时使用8.0.22版本是有深意的,MySQL :: MySQL Connector/J 8.0 Release Notes :: Changes in MySQL Connector/J 8.0.23 (2021-01-18, General Availability)
看来官方修复了。
来源码看看,,果然,不配置就客户端获取时区了TimeZone.getDefault();
    public void configureTimeZone() {
        //先读配置connectionTimeZone
        String connectionTimeZone = getPropertySet().getStringProperty(PropertyKey.connectionTimeZone).getValue();
        TimeZone selectedTz = null;
        //如果没配参数,或者参数配LOCAL,就取客户端时区
        //配置其他选择,基本上参数决定了时区,不再MySQL server去获取时区了
        if (connectionTimeZone == null || StringUtils.isEmptyOrWhitespaceOnly(connectionTimeZone) || "LOCAL".equals(connectionTimeZone)) {
            selectedTz = TimeZone.getDefault();
        } else if ("SERVER".equals(connectionTimeZone)) {
            // Session time zone will be detected after the first ServerSession.getSessionTimeZone() call.
            return;
        } else {
            selectedTz = TimeZone.getTimeZone(ZoneId.of(connectionTimeZone)); // TODO use ZoneId.of(String zoneId, Map<String, String> aliasMap) for custom abbreviations support
        }
        //设置时区
        this.serverSession.setSessionTimeZone(selectedTz);
        //默认不再强制把时区塞进session 的 Variables中
        if (getPropertySet().getBooleanProperty(PropertyKey.forceConnectionTimeZoneToSession).getValue()) {
            // TODO don't send 'SET SESSION time_zone' if time_zone is already equal to the selectedTz (but it requires time zone detection)
            StringBuilder query = new StringBuilder("SET SESSION time_zone='");
            ZoneId zid = selectedTz.toZoneId().normalized();
            if (zid instanceof ZoneOffset) {
                String offsetStr = ((ZoneOffset) zid).getId().replace("Z", "+00:00");
                query.append(offsetStr);
                this.serverSession.getServerVariables().put("time_zone", offsetStr);
            } else {
                query.append(selectedTz.getID());
                this.serverSession.getServerVariables().put("time_zone", selectedTz.getID());
            }
            query.append("'");
            sendCommand(this.commandBuilder.buildComQuery(null, query.toString()), false, 0);
        }
    }
再看看设置参数的地方,这里设计有点改变,通过QueryBindings接口抽象了处理逻辑
    public void setTimestamp(int parameterIndex, Timestamp x) throws java.sql.SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            ((PreparedQuery<?>) this.query).getQueryBindings().setTimestamp(getCoreParameterIndex(parameterIndex), x, MysqlType.TIMESTAMP);
        }
    }
实现com.mysql.cj.ClientPreparedQueryBindings
   public void bindTimestamp(int parameterIndex, Timestamp x, Calendar targetCalendar, int fractionalLength, MysqlType targetMysqlType) {
        if (fractionalLength < 0) {
            // default to 6 fractional positions
            fractionalLength = 6;
        }
        x = TimeUtil.adjustNanosPrecision(x, fractionalLength, !this.session.getServerSession().isServerTruncatesFracSecs());
        StringBuffer buf = new StringBuffer();
        if (targetCalendar != null) {
            buf.append(TimeUtil.getSimpleDateFormat("''yyyy-MM-dd HH:mm:ss", targetCalendar).format(x));
        } else {
            this.tsdf = TimeUtil.getSimpleDateFormat(this.tsdf, "''yyyy-MM-dd HH:mm:ss",
                    targetMysqlType == MysqlType.TIMESTAMP && this.preserveInstants.getValue() ? this.session.getServerSession().getSessionTimeZone()
                            : this.session.getServerSession().getDefaultTimeZone());
            buf.append(this.tsdf.format(x));
        }
        if (this.session.getServerSession().getCapabilities().serverSupportsFracSecs() && x.getNanos() > 0) {
            buf.append('.');
            buf.append(TimeUtil.formatNanos(x.getNanos(), 6));
        }
        buf.append('\'');
        setValue(parameterIndex, buf.toString(), targetMysqlType);
    }
时区就是刚刚设置的,亚洲/上海
总结
一个时区问题,居然里面有这么多玩头,MySQL居然在8.0.23才修复这个,难道MySQL认为大家都会配置时区,还是服务器都不使用CST时区。另外如果使用UTC时区,是一个精准的时区,表示0区时间,就会从一个坑跳另一个坑,,所以还是精准用Asia/Shanghai吧,或者驱动升级8.0.23及以上版本,不配置时区。
[转帖]mysql-connect-java驱动从5.x升级到8.x的CST时区问题的更多相关文章
- Mysql Java 驱动安装
		怎么安装MYSQL的JDBC驱动 1.下载mysql for jdbc driver. http://dev.mysql.com/downloads/connector/j/5.0.html 2.解压 ... 
- Mysql使用ReplicationDriver驱动实现读写分离
		数据库的主从复制环境已经配好,该要解决系统如何实现读写分离功能了.Mysql的jdbc驱动提供了一种实现ReplicationDriver. 1 数据库地址的两种写法 参考:https://dev.m ... 
- mysql与java的之间的连接
		package cn.hncu; //注意,以下都是sun公司的接口(类)---这样以后换成Oracle等其它数据库,代码不用动import java.sql.Connection;import ja ... 
- Android开发JDBC连接mysql数据库导入驱动方法
		在使用JDBC编程时需要连接数据库,导入JAR包是必须的,导入其它的jar包方法同样如此,导入的方法是 打开eclipse 1.右击要导入jar包的项目,点properties 2.左边选择java ... 
- MySQL的JDBC驱动源码解析
		原文: MySQL的JDBC驱动源码解析 大家都知道JDBC是Java访问数据库的一套规范,具体访问数据库的细节有各个数据库厂商自己实现 Java数据库连接(JDBC)由一组用 Java 编程语言 ... 
- mysql 的 java 连接库
		mysql 的 java 连接库 解压缩mysql-connector-java-5.1.30.zip 将要使用的是mysql-connector-java-5.1.30-bin-g.jar和mysq ... 
- IDEA用Maven连接MySQL的jdbc驱动,并操作数据库
		1.在IDEA里创建Maven项目 1.1.点击Create New Project 1.2.选择Maven,JDK这里用的是1.8,点击Next 1.3.填入“组织名”.“项目名”,版本是默认 ... 
- IDEA导入MySQL的jdbc驱动,并操作数据库
		将MySQL的jdbc驱动,导入IDEA的方式,虽然也能连接并且操作数据库,但并不推荐这种方式,推荐使用Maven工程的方式:https://www.cnblogs.com/dadian/p/1193 ... 
- MYSQL的Java操作器——JDBC
		MYSQL的Java操作器--JDBC 在学习了Mysql之后,我们就要把Mysql和我们之前所学习的Java所结合起来 而JDBC就是这样一种工具:帮助我们使用Java语言来操作Mysql数据库 J ... 
- Win7-64bit系统下安装mysql的ODBC驱动
		安装过mysql数据库后,有些软件在调用mysql数据库时不会直接调用,需要安装mysql数据库的ODBC驱动,再来调用.这里就介绍下,如何在win7系统下安装mysql的ODBC驱动. Win7系统 ... 
随机推荐
- electron入门之打包exe分发(五)
			electron入门到入土,前面配置阿里镜像加速.为了防止我们打包下载龟速,所以需要给electron配置阿里镜像加速.参考配置阿里云镜像https://blog.csdn.net/weixin_44 ... 
- Spring Eureka 源码解析
			本文将简要分析一下关于 Spring Eureka 相关的一些必要的源代码,对应的版本:Spring Cloud 2021.0.1 @EnableEurekaServer 注解 @EnableEure ... 
- AI与低代码解锁无限可能
			前言 近年来,人工智能(AI)和低代码开发技术逐渐成为数字化转型的重要推动力.AI作为一项具有革命性潜力的技术,正在改变我们生活的方方面面.而低代码开发则提供了一种快速构建应用程序的方法,使得开发者无 ... 
- Jenkins汉化配置
			登录进入Jenkins首页 输入:本地ip+端口号(localhost:8099) 进入插件管理页面(Manage Jenkins)安装相关插件 搜索:到available栏目搜索:Locale pl ... 
- EDS从小白到专家丨生态产业链高效协同的一计良策
			本文分享自华为云社区<[EDS从小白到专家]第3期:生态产业链高效协同的一计良策>,作者:开天aPaaS小助手 . 号外! 华为将于2023年9月20-22日,在上海世博展览馆和上海世博中 ... 
- JAVA性能优化- IntelliJ插件:java内存分析工具(JProfiler)
			JProfiler(Java性能分析神器) v11.1.4 下载 安装目录不要有空格 安装成功后,在 Intellij 里面选择对应的 jprofiler.exe 路径 点击下图JProfiler图标 ... 
- 手把手教你配置JupyterLab 环境
			Python 代码编辑器怎么选?PyCharm.VS Code.Jupyter Notebook 都各有特色. 对于大型代码库,最好还是用传统的 IDE 比较靠谱,但是数据分析等需要可视化操作的场景下 ... 
- 库函数 |  C++17 std::filesystem文件系统 用法指北
			本文将针对常用的场景,对 std::filesystem 的使用逐一进行验证: 判断文件夹是否存在 创建单层目录 逐级创建多层目录 创建多级目录 当前文件路径 创建文件"from.dat&q ... 
- Mac | HomeBrew 安装 & 配置 MySQL
			这个是我最新并且一直推崇的方法: 1.安装:brew install mysql 2.开启mysql:mysql.server start 3.使用mysql的配置脚本:/usr/local/opt/ ... 
- #627 DIV3 题解
			A. 每组给一个和个数(),每次操作可以给一个加2,求是否能使n个数相等 4 3 1 1 3 4 1 1 2 1 2 11 11 1 100 YES NO YES YES 直接扫一遍,记录最大值与当前 ... 
