其他
MySQL 的 timestamp 会存在时区问题?
The following article is from 扣钉日记 Author 扣钉日记
简介
timestamp 没有时区问题,而 datetime 有时区问题。原因是 timestamp 是以 UTC格式存储的,而 datetime 存储类似于时间字符串的形式; timestamp 也有时区问题。
基本概念
由于地域的限制,人们发明了时区的概念,用来适应人们在时间感受上的差异。比如中国的时区是东 8 区,表示为 +8:00,或 GMT+8。而日本的时区是东 9 区,表示为 +9:00,或 GMT+9,当中国是早上 8 点时,日本是早上 9 点,即东 8 区的 8 点与东 9 区的 9 点,这两个时间是相等的。
另外时间还有如下两个概念:
绝对时间:如 UNIX 时间戳,是 1970-01-01 00:00:00 开始到现在的秒数,如:1582416000,这种表示是绝对时间,不受时区影响,也叫纪元时 Epoch; 本地时间:相对于某一时区的时间,是本地时间。比如东 8 区的 2020-02-23 08:00:00,是中国人的本地时间。而在此时,日本人的本地时间是 2020-02-23 09:00:00。所以本地时间都是与某一时区相关的,脱离时区看本地时间,是没有意义的,因为你并不知道这具体是指的什么时间点。
timestamp 与 datetime 区别
CREATE TABLE `time_test` (
`id` bigint unsigned,
`time_stamp` timestamp,
`date_time` datetime,
`create_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_datetime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
)
然后,如下图手动插入一个固定时间的数据,以及用 now() 函数插入当前时间。
当插入完数据后,然后我们修改当前会话的时区为 +9:00,即日本的东 9 区,然后再次查看数据。
如上,定义为 timestamp 类型的列 time_stamp、create_timestamp 不管是手动插入的,还是 now() 函数插入的,东9区都比东 8 区的时间大 1 个小时。
我们将东 8 区的的 2020-02-23 08:00:00 转换为 UNIX 时间戳(绝对时间),再插入数据库试试。
如下,使用 Linux 的 date 命令转换时间串为 UNIX 时间戳:
$ "date" --date="2020-02-23 08:00:00 +08:00" +%s
1582416000
如上,查询出来的时间也是东 9 区的 9 点,时间也是正确的。
为什么网上又说 timestamp 类型存在时区问题?
然后,我写了两个接口 /insert 与 /queryAll 来插入与查询数据,如下:
然后,调用 /insert 接口插入数据。注意,我接口传入的时间是东 8 区的 8 点,如下:
插入完成后,去数据库中查询一把,如下:
可以看到,time_stamp 字段时间是 9 点。且我已将数据库时区设置为东 9 区,东 9 区的 9 点与东 8 区的 8 点,这两个时间实际是相等的,因此时间数据没错。
用 /queryAll 接口将数据查询出来,如下:
timeStamp 属性是 1582416000000,这是毫秒级的时间缀,秒级则是 1582416000,对应是东 8 区的 2020-02-23 08:00:00,时间数据也没错。
再查询一下数据,如下:
那为什么网上会说 timestamp 存在时区问题?
如图,我把数据库时区修改回 +9:00 时区,然后故意把 JDBC 的 URL 上的 serverTimezone 配置为与数据库不一致的 GMT+8 时区,然后重启 Java 应用,如下:
url: jdbc:mysql://localhost:3306/testdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8
重新插入数据。注意,插入的时间还是东 8 区的 8 点,如下:
time_stamp 时间竟然是 8 点!要知道我们虽然插入的是东 8 区的 8 点,但当前会话可是东 9 区的,东 8 区的 8 点等于东 9 区的 9 点,所以正确显示应该为 9 点才对,时间差了 1 小时!
然后,我又调用 /queryAll 接口查询了一把,想看看 MyBatis 查询出来的时间数据对不对,如下:
可以看到 timeStamp 是 1582416000000,秒级是 1582416000,这个时间就是东 8 区的 8 点,东 9 区的 9 点啊!
serverTimezone 的本质
//若使用普通驱动,使用此方法配置mysql连接的时区
com.mysql.jdbc.ConnectionImpl#configureTimezone()
//若使用cj驱动,使用此方法配置mysql连接的时区
com.mysql.cj.protocol.a.NativeProtocol#configureTimezone()
比如 SQL 参数是一个 Date 对象,时间值是东 8 区的 2020-02-23 08:00:00。注意它里面存储的可不是 2020-02-23 08:00:00 这个字符串,它是 Date 对象(绝对时间),只是我用文字表达出来是东8区的2020-02-23 08:00:00; 然后,由于 serverTimeZone 配置的是东 8 区,MySQL 驱动会将这个 Date 对象转为 2020-02-23 08:00:00。注意,这时已经是字符串了。然后,再将 SQL 发送给 MySQL。注意,这里的 SQL 里面已经将 Date 参数替换为 2020-02-23 08:00:00 了,因为 Date 对象本身是无法走网络的; 然后,MySQL 数据库接收到这个时间字符串 2020-02-23 08:00:00 后。由于数据库时区配置是东 9 区,它会认为这个时间是东 9 区的,它会以东 9 区解析这个时间字符串。这时,数据库保存的时间是东 9 区的 2020-02-23 08:00:00,也就是东 8 区的 2020-02-23 07:00:00,保存的时间就偏差了 1 个小时。
那么,如果我们将 serverTimezone 配置改正确,即与数据库保持一致时,应该查询到的时间就会是错的,会少 1 个小时。
url: jdbc:mysql://localhost:3306/testdb?serverTimezone=GMT%2B9&useUnicode=true&characterEncoding=utf8
返回的是毫秒级时间戳 1582412400000,秒级就是 1582412400。使用 Linux 的 date命令转换为时间字符串形式:
$ "date" --date="@1582412400" +"%F %T %z"
2020-02-23 07:00:00 +0800
另外一点是,当没有配置 serverTimezone 时,MySQL 驱动会自动读取 MySQL Server中配置的时区,这里面也有坑。
MySQL 安装好后默认时区是 SYSTEM。而 SYSTEM 指的是 system_time_zone 变量的时区,如下:
对于 Linux 或 MySQL,会认为 CST 是中国标准时间 (+8:00)。但 Java 却认为 CST 是美国标准时间 (-6:00) 注:可能和 Java 运行在 Windows 中有关
$ "date" +"%F %T %Z %z"
2021-09-12 18:35:49 CST +0800
Entity 中日期属性是 String 呢?
然后也写两个接口,/insert2 与 /queryAll2,如下:
然后插入数据。注意,这时我是直接将无时区的 8 点作为参数给到 SQL 的,如下:
然后再查询一把,如下:
如上所示,time_stamp 字段值是 8 点,但此时数据库时区是东 9 区,所以这是东 9 区的 8 点。
然后,将数据库与 JDBC 中 serverTimezone 都改为东 8 区,改完后重启Java应用。如下:
url: jdbc:mysql://localhost:3306/testdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8
再查询一把,如下:
如上所示,time_stamp 字段值是 8 点,但现在数据库时间是东 8 区,所以这是东 8 区的 8 点。
然后,再将 JDBC URL 上的 serverTimezone 调整为东 9 区,然后重启 Java 应用。如下:
url: jdbc:mysql://localhost:3306/testdb?serverTimezone=GMT%2B9&useUnicode=true&characterEncoding=utf8
现在 serverTimezone 与数据库中不一致,数据库是东 8 区,serverTimezone 是东 9 区。
再次插入无时区的 8 点,如下:
然后再查询一把,结果如下:
time_stamp 字段值还是 8 点,数据库是东 8 区,所以这是东 8 区的 8 点。我们 serverTimezone 与数据库的时区不一致啊,但却没看到时间有偏差,这又是为什么?
前面说过了,对于 JDBC 中的 Date 对象,在发送给 MySQL 前,会先根据 serverTimezone 转换为相应时区的时间字符串,但现在 Entity 中时间属性是 String 类型,MySQL 驱动不会进行转换,所以不管 serverTimezone 怎么配置,对 String 类型的时间串都没影响。
询问接口定义人员,这个接口的时间字符串指的是哪个时区的?比如是东 8 区的 2020-02-23 08:00:00; 然后接口接收到时间后,要以东 8 区将时间字符串转换为 Date 对象,如下: SimpleDateFormat sdf = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss');
sdf.setTimeZone(TimeZone.getTimeZone("GMT+8"));
Date date = sdf.parse("2020-02-23 08:00:00");如果 Entity 中时间属性定义的是 String,那么我们要再将 Date 对象以数据库的时区格式化为对应的时间字符串。比如,数据库时区是东 9 区,那么格式化后就是 2020-02-23 09:00:00,如下: SimpleDateFormat sdf = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss');
sdf.setTimeZone(TimeZone.getTimeZone("GMT+9"));
String dateStr = sdf.format(date);
entity.setTimeStamp(dateStr);最后,将 Entity 保存到 MySQL 中的,就也会是东 9 区的 2020-02-23 09:00:00,结果正确。
最佳实践
数据库中用 timestamp 还是 int 来存储时间?
但从某些角度看,这种方案只是把时区问题从数据库端推到应用端去了,时区问题将出现在将时间字符串转换为时间缀的过程中。比如某程序员从 API 接口中拿到时间字符串后,没考虑时区,直接转为 UNIX 时间缀,就可能出现时区问题。
因此,对于不带时区的时间串解析,一定要问清楚这是哪个时区的时间,并在代码中显式指定。
开发人员看到这个字段后,无法一目了然的了解到这个时间缀大概是个什么时间,需要去转换一下,会很繁琐; 像 update_time 这样的字段,数据库提供了DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 的机制,这样在更新任何字段时,update_time 会自动更新。而如果使用 int存储,就需要程序员每次更新表时,重新 set 这个字段,容易遗忘; 由于 int 只有 4 个字节,用它来存储时间,会在 2038 年后溢出,而对于 timestamp来说,MySQL 将其底层存储统一修改为 8 个字节,相对来说还是比较容易的。
总结
- EOF -
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能
点赞和在看就是最大的支持❤️