起因
之前因为mysql connector 5的某个版本有OOM的风险: 传送门
所以调整了timeout时间和升级了connector到6.
一切以为都是正常的,不过却出现了一场惊天动地的抢险修复?。
苦难
事情出在第二天,刚好周六。运营反馈说线上所有的贷款借据日期都不准了,差了14个小时。
于事在家里干紧vpn连上后,查看情况。
不看不知道,一看吓一跳,还真的TM都差了14个小时。这不是要人命吗,别说数据对不上,关这一天的利息就不知道多少钱了,想想就可怕啊。
分析
此时最需要的时冷静。待人静下心来,以及和同事沟通排查。这个程序上线半年多没出现这个问题,那么肯定是最近的更新导致的。最近的更新就是改了timeout和升级了 jdbc mysql connector的版本。timeout应该不会有问题,那问题很可能在connector上。
于是在本地新建一个工程用新版connector连上mysql试了一下,果然差了14个小时。
解决
问题找到了,那就要解决啊。一开始还真不知道怎么解决,问题找到了,然没有查到根本原因啊。后面查了查万能的google,发现在jdbc的url里指定timezone为东8区就可以了,如下
serverTimezone=GMT%2b8:00
这里的%2b是url encode后的+,decode后就是serverTimezone=GMT+8:00
原理
既然知道怎么解决了,在解决完问题,修复数据后,必须要查清楚具体原因。后来仔细排查下来,要jdk的文档中找到了答案。
先来看一下mysql的时区设置:
show variables like '%time_zone%';
system_time_zone CST
time_zone SYSTEM
可以看出system_time_zone是CST,也就是东8区,time_zone用的是SYSTEM,那也就是system_time_zone的CST。
然后再看一下connector里怎么处理时区的,在ConnectionImpl类中,有一个
private void initializePropsFromServer() throws SQLException
在里面可以看到初时化时区的方法
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 ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Connection.9", new Object[] { canonicalTimezone }),
getExceptionInterceptor());
}
}
this.defaultTimeZone = this.serverTimezoneTZ;
}
这里的PropertyDefinitions.PNAME_serverTimezone就是jdbc url上的serverTimezone变量,
可以很清楚地看出优先使用serverTimezon变量,如果没有,则用mysql的time_zone。
在没有serverTimezone变量时,读取mysql的time_zone为SYSTEM, 则进一步读取system_time_zone,也就是CST。
然后用CST做为时区,那问题来了,CST不就是东8区吗,怎么会有问题?
查看jdk的TimeZone类,看到注释中有这个一句:
* For compatibility with JDK 1.1.x, some other three-letter time zone IDs
* (such as "PST", "CTT", "AST") are also supported. However, <strong>their
* use is deprecated</strong> because the same abbreviation is often used
* for multiple time zones (for example, "CST" could be U.S. "Central Standard
* Time" and "China Standard Time"), and the Java platform can then only
* recognize one of them.
看到坑了没?CST可以是美国的中央时区,也可以是中国标准时区。所以现在问题已经找到了。
- 在mysql中CST代表的是中国标准时区,东8区。
- 在jdk中,CST代表的是美国中央时区
所以这里就出现在时区错乱的现象。
在jdk中,CTT表示东8区,所以也可以用serverTimezone=CTT,不过最好用GMT+8:00
ps:jdk版本1.8
------