你可能忽略的mysql时区问题

3,805 阅读5分钟

事情背景

最近做了一个小程序商城项目(点击可查看,欢迎关注交流),测试时发现mysql数据库中的时间字段有的比当前北京时间晚8个小时,有的却是正常的,不用想肯定是时区问题,但诡异的是并非所有时间都有问题... 经过一番排查,发现问题的根本在于小小的时区问题牵涉的不止数据库,这里的坑还真不少,索性总结一下。

项目系统配置介绍

先简单交代一下这个项目相关的配置:
1、后端使用Java springboot框架,连接mysql的配置如下

jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

2、mysql是用docker直接部署的,启动命令如下:

docker run --rm --name mysql-test -e MYSQL_ROOT_PASSWORD=123456 -d mysql:latest

mysql时间字段类型是datetime,用CURRENT_TIMESTAMP设置了默认值,例如:

`add_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'

问题分析

我们先分析一下都有哪些地方涉及时区的问题。

1、mysql本身的时区

我们登录mysql看看时区是啥,查询时区的语句如下:

mysql> show variables like '%time_zone%';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | UTC    |
| time_zone        | SYSTEM |
+------------------+--------+
2 rows in set (0.29 sec)

mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2021-05-12 11:41:05 |
+---------------------+
1 row in set (0.01 sec)

果然,time_zone说明mysql使用system的时区,system_time_zone说明system使用UTC时区,查到的时间确实晚了8个小时。
现在确定了mysql的时区确实有问题,但依然无法解释为啥有的数据时间是正常的

2、jdbc连接设置

我们再来看看jdbc连接串,发现其中有一个时区参数serverTimezone=Asia/Shanghai。

jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

3、分析结论

现在,就可以解释为啥有的时间正常,有的不正常了:

  1. 由于mysql本身的时区有问题,所以用CURRENT_TIMESTAMP设置了默认值的字段是通过mysql写入的自然会有问题
  2. 由于jdbc连接串设置了时区参数,所以通过Java程序写入数据库的时间是正常的


由此,我们可以看到时区的问题涉及到了mysql本身及其所处的系统(这里是docker,docker镜像默认的时区就是UTC),还有jdbc代码层面。下面就从这3个方面具体说说怎么彻底解决时区问题

解决方案

1、设置docker系统的时区

我们首先要保证mysql所处系统(这里是docker)的时区是正确的,设置docker的时区有两种方式

方法一:映射本机的/etc/localtime(这种方式要保证docker所处的系统时区是正确的)
docker run --rm --name mysql-test -e MYSQL_ROOT_PASSWORD=123456  -v /etc/localtime:/etc/localtime -d mysql:latest

方法二:设置环境变量-e TZ=Asia/Shanghai
docker run --rm --name mysql-test -e MYSQL_ROOT_PASSWORD=123456 -e TZ=Asia/Shanghai -d mysql:latest

修改启动命令后再来看看mysql的时区

mysql> show variables like '%time_zone%';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | CST    |
| time_zone        | SYSTEM |
+------------------+--------+
2 rows in set (0.00 sec)

好了,变成CST了,看看生成的时间好像一切都正常了,但是真的一定没问题吗,搜索一下CST的含义你就会发现问题。原来CST居然能表示四种时区,分别为:
1.美国中部时间 Central Standard Time (USA) UTC-05:00 / UTC-06:00
2.澳大利亚中部时间 Central Standard Time (Australia) UTC+09:30
3.中国标准时间 China Standard Time UTC+08:00
4.古巴标准时间 Cuba Standard Time UTC-04:00
这就导致jdbc去获取时区的时候,有可能就会出现问题,比如有可能把CST解析为UTC-5或者UTC-6时区,所以设置jdbc连接的时区参数也是很有必要的。

2、设置mysql的时区

通过第一步我们发现仅仅设置系统的时区是不够的,所以强烈建议把mysql的时区直接设置为+8:00(即我们所处的东8区),修改mysql的时区也有两种方式,可看情况使用
1.临时修改

set global time_zone = '+8:00'; #修改mysql全局时区为北京时间,但是重启mysql服务这个设置依然会失效
set time_zone = '+8:00'; #修改当前会话时区
flush privileges; #立即生效

2.永久修改
修改配置文件【linux:my.cnf,windows:my.ini】
在[mysqld]区域部分加上:default-time_zone = '+8:00'
然后重启mysql服务即可。

修改后看一下mysql时区,变成了+08:00

mysql> show variables like '%time_zone%';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | CST    |
| time_zone        | +08:00 |
+------------------+--------+
2 rows in set (0.11 sec)

3、设置jdbc连接的时区参数

从我这个事情可以看出,不管mysql的时区对不对,如果jdbc读到的时区不正确就会导致写入数据库的时间不对,所以仅仅通过前两步并不能保证万无一失,因此一定要在jdbc连接串中加上时区参数serverTimezone=Asia/Shanghai

最后概括一下避免时区问题的方法:

  1. 首先保证系统时区正确
  2. jdbc连接中添加时区参数serverTimezone=Asia/Shanghai
  3. time_zone参数建议尽量设置为+8:00,不要使用有歧义的CST(即使是系统默认的_SYSTEM_值)。另外尽量使用datetime类型而不是timestamp类型,这个建议跟字段类型也有关系,这里就不展开阐述了,如有问题欢迎与我交流


文章开头提到的项目地址,欢迎关注交流:
Github:github.com/frank-say/b…
Gitee:gitee.com/frank-say/b…
点击体验