背景
最近新开一个项目,创建工程时为了与时俱进,springboot使用了2.3.3.RELEASE,orm框架使用mybatis,mybatis-spring-boot-starter相应的使用了2.1.3,mybatis generator使用了1.4.0。
然后就是巴拉巴拉码代码(顺便吐槽下mabatis-generator使用MyBatis3DynamicSql方式生成的代码,使用起来跟jooq还真特么像啊哈哈。)
好了,代码码好开始自测了,结果发现问题了
- 保存记录的时候,mysql表的timestamp字段保存的值不对,数据库保存的值比实际时间慢13小时。
- 查询记录的时候,查出的结果比数据库保存的值又快了5个小时。
把mybatis的sql打印出来,发现保存记录的时候传给mysql的时间是对的。这就奇怪了,而且更奇怪的是,13个小时和5个小时这是什么鬼,跟UTC时间也扯不上边啊。
解决过程
- 寻找差异
- 最初怀疑是我使用的开发库是不是时区设置的不对
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b8qQ2FX8-1600052839667)(http://pfp.ps.netease.com/kmspvt/file/5f5c728b68d8646962dbc50cZr953YvB01?sign=UqEjuXI1d5xySGlyBru6Iv8IwAs=&expire=1600054416)]
跟测试库和其他工程开发库对比了一下,发现没差别。所以问题不是出自这里。但是还是有个疑惑啊,什么是CST时区呢。
搜索引擎了一下
什么鬼,居然CST可以视为四个时区的时间,这么说假设CST使用了美国中部时间的话(为啥不假设用古巴标准时间呢?美帝开发的软件,可能会精古吗,直接排除。。。),那咱是UT+8,它是UT-6,那存到库里的时间应该是中国时间-14,这虽然接近13,但是毕竟不是啊,刚以为看到了曙光,结果又被打击了。无论如何这是个线索啊,google一下吧。CST (时区缩写) 编辑 CST可视为美国、澳大利亚、古巴或中国的标准时间. 美国中部时间: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
daylight saving time是啥啊,原谅我的无知,继续搜索引擎吧The North American Central Time Zone (CT) is a time zone in parts of Canada, the United States, Mexico, Central America, some Caribbean Islands, and part of the Eastern Pacific Ocean. Central Standard Time (CST) is six hours behind Coordinated Universal Time (UTC). During summer most of the zone uses daylight saving time (DST), and changes to Central Daylight Time (CDT) which is five hours behind UTC.[1]
必须承认,看到这个结果的时候我的心里真是五味杂陈,啊,夏令时,一个埋藏在记忆深处的名词,一个多么暴露年龄的名词啊。原来mysql给我保存时间的时候,不仅用了美帝的时区,还用了美帝的夏令时。。。夏令时,表示为了节约能源,人为规定时间的意思。也叫夏时制,夏时令(Daylight Saving Time:DST),又称“日光节约时制”和“夏令时间”,在这一制度实行期间所采用的统一时间称为“夏令时间”。
可是可是,为什么之前的其他工程使用springboot1.5.9没出现过这种问题呢。联想到mysql-connector-java包升级到了8.0.15,连spring.datasource.driverClassName都跟以前不一样,变成com.mysql.cj.jdbc.Driver了,那是不是时区设置方面,也发生了某些不兼容的变化呢。。。 - 最初怀疑是我使用的开发库是不是时区设置的不对
- 翻文档
既然怀疑是mysql-connector-java包升级导致的问题,那就去翻翻mysql 8的文档吧,看看有没有什么跟时区有关的内容。结果找到这么一段
看到这里大概的意思是想要时区正常,您要么修改mysql server的配置,要么您就要在连接url后边加上正确的serverTimezone。wtf,为啥呢,为啥呢。那反正我是没权限改服务器配置的,只能选择后者呢。然而还是想问,为啥呢。没其他的办法,看代码吧Connector/J 8.0 always performs time offset adjustments on date-time values, and the adjustments require one of the following to be true: The MySQL server is configured with a canonical time zone that is recognizable by Java (for example, Europe/Paris, Etc/GMT-5, UTC, etc.) The server's time zone is overridden by setting the Connector/J connection property serverTimezone (for example, serverTimezone=Europe/Paris).
3.看代码
下面是5.1.46的代码
private void configureTimezone() throws SQLException {
String configuredTimeZoneOnServer = this.serverVariables.get("timezone");
if (configuredTimeZoneOnServer == null) {
configuredTimeZoneOnServer = this.serverVariables.get("time_zone");
if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
configuredTimeZoneOnServer = this.serverVariables.get("system_time_zone");
}
}
String canonicalTimezone = getServerTimezone();
if ((getUseTimezone() || !getUseLegacyDatetimeCode()) && 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 SQLError.createSQLException(iae.getMessage(), SQLError.SQL_STATE_GENERAL_ERROR, getExceptionInterceptor());
}
}
}
if (canonicalTimezone != null && canonicalTimezone.length() > 0) {
this.serverTimezoneTZ = TimeZone.getTimeZone(canonicalTimezone);
下面是8.0.15的
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));
看起来注册时区的逻辑还是差不多的,先取服务器参数time_zone,如果是SYSTEM,就取服务器参数system_time_zone;然后如果url里没有指定serverTimeZone,就拿system_time_zone作为时区了,所以说两个版本里最终取到的时区都是CST。wtf,那就继续看看保存数据的代码吧。
5.1.46的,代码取自PreparedStatement、ConnectionImpl、TimeUtil
public void setTimestamp(int parameterIndex, Timestamp x) throws java.sql.SQLException {
synchronized (checkClosed().getConnectionMutex()) {
setTimestampInternal(parameterIndex, x, null, this.connection.getDefaultTimeZone(), false);
}
}
跟着this.connection.getDefaultTimeZone()一直往上走,最终
private static final TimeZone DEFAULT_TIMEZONE = TimeZone.getDefault();
哦,原来5.1.46里面保存timestamp字段时,时区是直接取了系统的时区。
那再看8.0.15的,代码来自PreparedStatementWrapper,ClientPreparedQueryBindings
@Override
public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
try {
if (this.wrappedStmt != null) {
((PreparedStatement) this.wrappedStmt).setTimestamp(parameterIndex, x);
} else {
throw SQLError.createSQLException(Messages.getString("Statement.AlreadyClosed"), MysqlErrorNumbers.SQL_STATE_GENERAL_ERROR,
this.exceptionInterceptor);
}
} catch (SQLException sqlEx) {
checkAndFireConnectionError(sqlEx);
}
}
@Override
public void setTimestamp(int parameterIndex, Timestamp x, Calendar targetCalendar, int fractionalLength) {
this.tsdf = TimeUtil.getSimpleDateFormat(this.tsdf, "''yyyy-MM-dd HH:mm:ss", targetCalendar,
targetCalendar != null ? null : this.session.getServerSession().getDefaultTimeZone());
/** c.f. getDefaultTimeZone(). this value may be overridden during connection initialization */
private TimeZone defaultTimeZone = TimeZone.getDefault();
咦,这里也是TimeZone.getDefault(); ,我*&¥#&*#,别激动别激动,看看注释,再回去看看注册时区的代码,原来是defaultTimeZone在那里被修改了;好了,现在知道了,8.0.15保存timestamp字段的时候是使用了mysql或者用户配置的时区。
然而,难道5.1.46就不用mysql或者用户配置的时区吗,那不是大bug吗,好吧回去继续看代码,代码取自PreparedStatement、TimeUtil
private void setTimestampInternal(int parameterIndex, Timestamp x, Calendar targetCalendar, TimeZone tz, boolean rollForward) throws SQLException {
if (!this.useLegacyDatetimeCode) {
newSetTimestampInternal(parameterIndex, x, targetCalendar);
} else {
Calendar sessionCalendar = this.connection.getUseJDBCCompliantTimezoneShift() ? this.connection.getUtcCalendar()
: getCalendarInstanceForSessionOrNew();
x = TimeUtil.changeTimezone(this.connection, sessionCalendar, targetCalendar, x, tz, this.connection.getServerTimezoneTZ(), rollForward);
原来setTimestampInternal里还有玄机,当useLegacyDatetimeCode是true的时候(默认为true),会修改时区,看到this.connection.getServerTimezoneTZ()没,这就是前面注册时区的时候获取的mysql服务器或者url里配置的时区
public static Timestamp changeTimezone(MySQLConnection conn, Calendar sessionCalendar, Calendar targetCalendar, Timestamp tstamp, TimeZone fromTz,
TimeZone toTz, boolean rollForward) {
if ((conn != null)) {
if (conn.getUseTimezone()) {
细节就不管了,总之useTimeZone为true时,就按照时区toTz来修正时间,嗯,结论就是如果你在url里配置上useTimeZone=true,5.1.46也也一样可以给你保存成美帝夏令时。。。。
代码研究完了,那在url里加上serverTimezone=Asia/Shanghai吧。来,自测一下,保存的时间对了。好!很有精神!
那再顺便测一下查询记录吧
。。。。。。
。。。。。。
。。。。。。
纳尼,什么,WHAT,获取到的时间比数据库保存时间慢了8小时,哈哈哈哈,我差点都忘了,还有问题2没有解决呢。
欲知后事如何,请听下回分解。