https://juejin.cn/post/7029291622537887774
 

前言

旧项目MySQL Java升级驱动,本来一切都好好的,但是升级到8.x的驱动后,发现入库的时间比实际时间相差13个小时,这就很奇怪了,如果相差8小时,那么还可以说是时区不对,从驱动源码分析看看

1. demo

pom依赖,构造一个真实案例,这里的8.0.22版本

 
html

复制代码
    <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

 
java

复制代码
@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);
}

数据库设计

 
sql

复制代码
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

 
java

复制代码
    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());
}
} }

规范时区

 
java

复制代码
   /**
* 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)  //根源就是这几句代码

 
java

复制代码
    public static TimeZone getTimeZone(String var0) {
return ZoneInfoFile.getZoneInfo(var0);
}

开始初始化,

 
复制代码
sun.timezone.ids.oldmapping 这个一般不会设置

读取$JAVA_HOME/lib/tzdb.dat,这是一个JDK时区存储文件

其中PRC就是中国时区,但是这个文件并未定义CST

CST在这里定义的

 
scss

复制代码
addOldMapping();
 
java

复制代码
    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是啥呢

 
arduino

复制代码
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"}

 
java

复制代码
    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

 
bash

复制代码
   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();

 
java

复制代码
    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接口抽象了处理逻辑

 
java

复制代码
    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

 
java

复制代码
   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时区问题的更多相关文章

  1. Mysql Java 驱动安装

    怎么安装MYSQL的JDBC驱动 1.下载mysql for jdbc driver. http://dev.mysql.com/downloads/connector/j/5.0.html 2.解压 ...

  2. Mysql使用ReplicationDriver驱动实现读写分离

    数据库的主从复制环境已经配好,该要解决系统如何实现读写分离功能了.Mysql的jdbc驱动提供了一种实现ReplicationDriver. 1 数据库地址的两种写法 参考:https://dev.m ...

  3. mysql与java的之间的连接

    package cn.hncu; //注意,以下都是sun公司的接口(类)---这样以后换成Oracle等其它数据库,代码不用动import java.sql.Connection;import ja ...

  4. Android开发JDBC连接mysql数据库导入驱动方法

    在使用JDBC编程时需要连接数据库,导入JAR包是必须的,导入其它的jar包方法同样如此,导入的方法是 打开eclipse 1.右击要导入jar包的项目,点properties 2.左边选择java ...

  5. MySQL的JDBC驱动源码解析

    原文:   MySQL的JDBC驱动源码解析 大家都知道JDBC是Java访问数据库的一套规范,具体访问数据库的细节有各个数据库厂商自己实现 Java数据库连接(JDBC)由一组用 Java 编程语言 ...

  6. mysql 的 java 连接库

    mysql 的 java 连接库 解压缩mysql-connector-java-5.1.30.zip 将要使用的是mysql-connector-java-5.1.30-bin-g.jar和mysq ...

  7. IDEA用Maven连接MySQL的jdbc驱动,并操作数据库

    1.在IDEA里创建Maven项目 1.1.点击Create New Project   1.2.选择Maven,JDK这里用的是1.8,点击Next  1.3.填入“组织名”.“项目名”,版本是默认 ...

  8. IDEA导入MySQL的jdbc驱动,并操作数据库

    将MySQL的jdbc驱动,导入IDEA的方式,虽然也能连接并且操作数据库,但并不推荐这种方式,推荐使用Maven工程的方式:https://www.cnblogs.com/dadian/p/1193 ...

  9. MYSQL的Java操作器——JDBC

    MYSQL的Java操作器--JDBC 在学习了Mysql之后,我们就要把Mysql和我们之前所学习的Java所结合起来 而JDBC就是这样一种工具:帮助我们使用Java语言来操作Mysql数据库 J ...

  10. Win7-64bit系统下安装mysql的ODBC驱动

    安装过mysql数据库后,有些软件在调用mysql数据库时不会直接调用,需要安装mysql数据库的ODBC驱动,再来调用.这里就介绍下,如何在win7系统下安装mysql的ODBC驱动. Win7系统 ...

随机推荐

  1. electron入门之打包exe分发(五)

    electron入门到入土,前面配置阿里镜像加速.为了防止我们打包下载龟速,所以需要给electron配置阿里镜像加速.参考配置阿里云镜像https://blog.csdn.net/weixin_44 ...

  2. Spring Eureka 源码解析

    本文将简要分析一下关于 Spring Eureka 相关的一些必要的源代码,对应的版本:Spring Cloud 2021.0.1 @EnableEurekaServer 注解 @EnableEure ...

  3. AI与低代码解锁无限可能

    前言 近年来,人工智能(AI)和低代码开发技术逐渐成为数字化转型的重要推动力.AI作为一项具有革命性潜力的技术,正在改变我们生活的方方面面.而低代码开发则提供了一种快速构建应用程序的方法,使得开发者无 ...

  4. Jenkins汉化配置

    登录进入Jenkins首页 输入:本地ip+端口号(localhost:8099) 进入插件管理页面(Manage Jenkins)安装相关插件 搜索:到available栏目搜索:Locale pl ...

  5. EDS从小白到专家丨生态产业链高效协同的一计良策

    本文分享自华为云社区<[EDS从小白到专家]第3期:生态产业链高效协同的一计良策>,作者:开天aPaaS小助手 . 号外! 华为将于2023年9月20-22日,在上海世博展览馆和上海世博中 ...

  6. JAVA性能优化- IntelliJ插件:java内存分析工具(JProfiler)

    JProfiler(Java性能分析神器) v11.1.4 下载 安装目录不要有空格 安装成功后,在 Intellij 里面选择对应的 jprofiler.exe 路径 点击下图JProfiler图标 ...

  7. 手把手教你配置JupyterLab 环境

    Python 代码编辑器怎么选?PyCharm.VS Code.Jupyter Notebook 都各有特色. 对于大型代码库,最好还是用传统的 IDE 比较靠谱,但是数据分析等需要可视化操作的场景下 ...

  8. 库函数 | C++17 std::filesystem文件系统 用法指北

    本文将针对常用的场景,对 std::filesystem 的使用逐一进行验证: 判断文件夹是否存在 创建单层目录 逐级创建多层目录 创建多级目录 当前文件路径 创建文件"from.dat&q ...

  9. Mac | HomeBrew 安装 & 配置 MySQL

    这个是我最新并且一直推崇的方法: 1.安装:brew install mysql 2.开启mysql:mysql.server start 3.使用mysql的配置脚本:/usr/local/opt/ ...

  10. #627 DIV3 题解

    A. 每组给一个和个数(),每次操作可以给一个加2,求是否能使n个数相等 4 3 1 1 3 4 1 1 2 1 2 11 11 1 100 YES NO YES YES 直接扫一遍,记录最大值与当前 ...