[转帖]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 直接扫一遍,记录最大值与当前 ...