Mysql8.0 jdbc驱动时区问题
背景
我们公司在升级Apollo1.8.1版本之后,发Item表的创建时间和更新时间字段与portal上展示差了13个小时,现象如下:
Item表结构:
排查过程
刚开始我们想到肯定是数据库的时区与Adminservice服务器时间的时区不一致导致的。
Adminservice所在的服务器时区是CST中国标准时间:
Mysql服务器的时区如下:
发现服务器和mysql服务器的时区是一致的,在加上和运维沟通后他们最近也没有升级服务器相关,应该不是这里的问题。
排查jpa关于时间戳的转换
查看关于jpa时间戳的准换还是用了最基本的方式去转换时间戳,不涉及时区的转换,代码如下
org.hibernate.type.TimestampType
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48public class TimestampType
extends AbstractSingleColumnStandardBasicType<Date>
implements VersionType<Date>, LiteralType<Date> {
public static final TimestampType INSTANCE = new TimestampType();
public TimestampType() {
super( TimestampTypeDescriptor.INSTANCE, JdbcTimestampTypeDescriptor.INSTANCE );
}
public String getName() {
return "timestamp";
}
public String[] getRegistrationKeys() {
return new String[] { getName(), Timestamp.class.getName(), java.util.Date.class.getName() };
}
public Date next(Date current, SharedSessionContractImplementor session) {
return seed( session );
}
public Date seed(SharedSessionContractImplementor session) {
return new Timestamp( System.currentTimeMillis() );
}
public Comparator<Date> getComparator() {
return getJavaTypeDescriptor().getComparator();
}
public String objectToSQLString(Date value, Dialect dialect) throws Exception {
final Timestamp ts = Timestamp.class.isInstance( value )
? ( Timestamp ) value
: new Timestamp( value.getTime() );
// TODO : use JDBC date literal escape syntax? -> {d 'date-string'} in yyyy-mm-dd hh:mm:ss[.f...] format
return StringType.INSTANCE.objectToSQLString( ts.toString(), dialect );
}
public Date fromStringValue(String xml) throws HibernateException {
return fromString( xml );
}
}mysql jdbc连接排查
由于之前的apollo1.5.2版本和1.8.1版本,升级了myql驱动,由之前的5.1.46升级到8.0.16,因此怀疑是不是mysql驱动导致的时区问题。然后看到网上确实还真是这个原因,参考文档https://blog.csdn.net/weixin_41787459/article/details/105790044
查看驱动关于timezone的源码如下
com.mysql.cj.jdbc.ConnectionImpl
1
2
3
4
5private void initializePropsFromServer() throws SQLException {
//......
this.session.getProtocol().initServerSession();
//......
}com.mysql.cj.protocol.a.NativeProtocol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39public void initServerSession() {
configureTimezone();
//......
}
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));
//
// 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.serverSession.getServerTimeZone().getID().equals("GMT")) {
throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Connection.9", new Object[] { canonicalTimezone }),
getExceptionInterceptor());
}
}
this.serverSession.setDefaultTimeZone(this.serverSession.getServerTimeZone());
}追踪代码可知,当 MySQL 的 time_zone 值为 SYSTEM 时,会取 system_time_zone 值作为协调时区。
重点在这里!若 String configuredTimeZoneOnServer 得到的是 CST 那么 Java 会误以为这是 CST -0500 ,因此 TimeZone.getTimeZone(canonicalTimezone) 会给出错误的时区信息。
本机默认时区是
Asia/Shanghai +0800
,误认为服务器时区为CST -0500
,实际上服务器是CST +0800
。Timestamp 被转换为会话时区的时间字符串了。问题到此已然明晰:
JDBC 误认为会话时区在 CST-5
JBDC 把 Timestamp+0 转为 CST-5 的 String-5
MySQL 认为会话时区在 CST+8,将 String-5 转为 Timestamp-13
最终结果相差 13 个小时!如果处在冬令时还会相差 14 个小时。
解决方案
jdbc配置连接指定时区(推荐)
1
2
3
4
5
6
7
8#原有配置
#jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8
#指定为东八区(北京时间)
jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=GMT%2B8
#或指定为上海时间
#jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai修改数据库时区(不推荐使用,可能影响其他程序)
1
2
3
4
5#查询数据库时区
show variables like '%time_zone'
#在my.cnf文件里指定时区,添加下行代码
default-time_zone = '+8:00