最近将mysql的驱动MySQL Connector/J版本升级到8.x后,突然发生了大bug,时间比较的都乱套了。
检查
排查后发现,mysql从命令行读取到的值,和实际时间相比,早了14个小时。立刻猜到是时区问题,然而,检查服务器和客户端的时区配置,其实是没问题的。
1 | mysql> show variables like '%time_zone%'; |
处理
既然在老版本没问题,配置也正常,肯定就是各个地方的兼容问题,那简单的方式就是直接手动指定一下。覆盖了事。
方法1 修改数据库配置
临时修改
控制台直接执行下面的语句
1 | set global time_zone = '+08:00'; |
记得改完后,客户端要重新链接才能取到新的值。
永久修改
在my.cnf的 mysqld节下面加上
1 | default-time-zone='+08:00' |
需要重启mysql才生效。
方法2 修改客户端配置
在某些公司,可能修改数据库的配置不是那么容易的,这种情况下,修改客户端配置就是比较好的选择了。只需要在jdbc url里加上
serverTimezone=Asia/Shanghai
这里的时区需要与服务器的真实时区相同
源码分析
客户端源码分析
在5.7中,处理时区的逻辑在 com.mysql.jdbc.ConnectionImpl这个类中。
1 | private void configureTimezone() throws SQLException { |
而8.x则在 com.mysql.cj.protocol.a.NativeProtocol 这个类里
1 | public void configureTimezone() { |
在新老版本中,时区设置的优先级均为url里的servertimezone字段,服务器里的timezone变量,systemtimezone变量。然而,在新版本中,默认就是使用服务器上的时区设置,除非url里显示的设置了serverTimezone参数。而按照上面服务的配置,就是要设置时区为CST。
然而很不幸的是,CST这种时区缩写,是很不靠谱的, 这个缩写可能对应了 “Central Standard Time”, “China Standard Time”, “Cuba Standard Time”,在java中,这个缩写默认情况下会去 sun.util.calendar.ZoneInfoFile里查找,具体定义如下
1 | {{"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变成了芝加哥时间。
服务端源码分析。
我们可以顺便看看服务端的system_time_zone值为啥是cst。这个变量的值,设置的地方在mysqld.cc的init_common_variables 函数中
首先调用1
tzset();
然后取tzname里的值。1
2
3
4
5
6
7
strmake(system_time_zone, _tzname[tm_tmp.tm_isdst != 0 ? 1 : 0],
sizeof(system_time_zone) - 1);
strmake(system_time_zone, tzname[tm_tmp.tm_isdst != 0 ? 1 : 0],
sizeof(system_time_zone) - 1);
tzset的函数定义在 tzset.c文件中,他最终会去调用 tzfile.c里的__tzfile_read函数里,通过这个函数名,我们也可以猜到他就是去读文件。而实际上也确实如此,它最终是去linux的时区配置文件夹 /usr/share/zoneinfo/ 中去读取 asia/shanghai文件。 tzfile的结构,这里就不再详细叙述了,可以粗略的看一下
cat /usr/share/zoneinfo/Asia/Shanghai
可以看到CST-8字样(注意不是+8),其中的CST就是缩写。