问题
新项目中的 JDBC 驱动使用了较新的 6.0.x 版本,写入数据库的时间与客户端当前时间相差 13 个小时。
以前碰到过相差 8 个小时的,是因为未配时区默认为UTC 标准时间,即UTC 0 时区的时间,即伦敦时间,与北京正好相差 8 个小时,这好解决。而相差 13 小时就让人疑惑了。
排查
- 排查确定客户端系统与服务器的系统时间一致,都为中国时区相同的时间。
- 排序查程序 new Date() 时间与当前系统时间一致。
- 排查数据库获取到的时间与服务器的系统时间一致。
以前问题确定基本可以排除服务器系统时间不一致问题。记得老项目使用使用的JDBC驱动是 5.1.x 版本,没出现这样的问题,就将6.0.x 降到 5.1.x ,问题解决。也就是说问题出在新的 6.0.x 版本 JDBC 驱动上。
解决
- 降低 JDBC 驱动版本为 5.1.37/5.1.38/5.1.39。 5.x 版本取的时区是系统时区,所以没有问题。
修改 Mysql 服务器上的默认的时区的配置,指定为 UTC 时区 东八区,中国即为 GMT+0800。
执行SQL修改,但数据库服务重启后会恢复默认。mysql> set global time_zone = '+08:00'; mysql> set time_zone = '+08:00';
修改
/etc/my.cnf
中,在[mysqld]
节点下增加default-time-zone='+08:00'
配置。数据库服务重启后仍有效。[mysqld] default-time_zone = '+8:00'
在 JDBC 的连接串中添加配置:
&serverTimezone=Asia/Shanghai
(北京时区)。// UTC 时区:serverTimezone=UTC,写入到数据库的时间会比当前北京时间早8个小时 // 香港时区:serverTimezone=Hongkong // 指定时间为东八区:serverTimezone=GMT%2B8 spring.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/dev?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&serverTimezone=GMT%2B8 // 指定使用 亚洲/上海 时间:serverTimezone=Asia/Shanghai spring.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/dev?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&serverTimezone=Asia/Shanghai
关闭 SSL 连接警告:
useSSL=false
原因
根本原因是出在新版本 JDBC 修改了与 MySQL 协商时区取值导致的。
MySQL 时区
mysql> SHOW VARIABLES LIKE '%time_zone%'; +------------------+--------+ | Variable_name | Value | +------------------+--------+ | system_time_zone | CST | | time_zone | SYSTEM | +------------------+--------+ 2 rows in set (0.04 sec)
MySQL 采用的是系统时区,系统时区是 CST,中文操作系统默认的 CST 时区为
中国标准时间
,即为 UTC 的东八区时间(CST +0800) 。
CST 时区
CST 时区是一个很混乱的时区,在与 MySQL 协商会话时区时,Java 会误以为是 CST -0500,而非 CST +0800。
CST 时区可以为如下4个不同的时区的缩写:
美国中部时间: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
假如今天是4月28日。美国从3月11日至11月7日实行夏令时,美国中部时间改为 UTC-05:00,与 UTC+08:00 相差 13 小时。
JDBC 获取时区
在 JDBC 与 MySQL 开始建立连接时,会调用 com.mysql.cj.jdbc.ConnectionImpl.initializePropsFromServer()
获取服务器参数,看到调用 this.session.configureTimezone()
函数,它负责配置时区。
public void configureTimezone() {
String configuredTimeZoneOnServer = getServerVariable("time_zone");
if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
configuredTimeZoneOnServer = getServerVariable("system_time_zone");
}
String canonicalTimezone = getPropertySet().getStringReadableProperty(PropertyDefinitions.PNAME_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.serverTimezoneTZ = 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...
if (!canonicalTimezone.equalsIgnoreCase("GMT")
&& this.serverTimezoneTZ.getID().equals("GMT")) {
throw ...
}
}
this.defaultTimeZone = this.serverTimezoneTZ;
}
跟踪源码可知当 MySQL 的 time_zone
值为 SYSTEM
时,会取system_time_zone
值作为协调时区。
问题核心:当 configuredTimeZoneOnServer
获取到的是CST
, Java 会误认为是 CST -0500
,因此 TimeZone.getTimeZone(canonicalTimezone)
会给出的就不是我们想要的北京时区(GMT+8)。
相关参考
注意:本文归作者所有,未经作者允许,不得转载