名词解释
简称 | 全称 | 说明 |
---|---|---|
GMT | Greenwich Mean Time | 格林威治标准时间 ; 英国伦敦格林威治定为0°经线开始的地方 |
UTC | Coordinated Universal Time | 世界协调时间;经严谨计算得到的时间,精确到秒,误差在0.9s以内, 是比GMT更为精确的世界时间 |
CST | 四个不同时区的缩写: 1. Central Standard Time (USA) UT-6:00 美国标准时间 2. Central Standard Time (Australia) UT+9:30 澳大利亚标准时间 3. China Standard Time UT+8:00 中国标准时间 4. Cuba Standard Time UT-4:00 古巴标准时间 | 在中国说CST是UTC+8,在美国说CST是UTC-6... |
DST | Daylight Saving Time | 夏季节约时间,即夏令时;是为了利用夏天充足的光照而将时间调早一个小时,北美、欧洲的许多国家实行夏令时 |
时区配置
查看linux时区
shell
# date -R
Wed, 11 May 2022 10:27:53 +0800
查看mysql时区
scss
SELECT @@GLOBAL.time_zone, @@SESSION.time_zone;
查看jvm时区
ini
# jps
559464 Jps
288513 jar
# ./jinfo 288513 | grep timezone
user.timezone = Asia/Shanghai
serverTimeZone参数
jdbc连接串中有一个serverTimeZone参数,作用是指定web服务器和mysql服务器的会话期间的mysql服务器时区,即临时指定mysql服务器的时区。
For Connector/J 8.0.23 and later, serverTimezone is an alias for connectionTimeZone. For Connector/J 8.0.22 and earlier, serverTimezone was used to override the session time zone setting on the server. Former connection option 'serverTimezone' is still valid as an alias of this one but may be deprecated in the future.
serverTimeZone的值
LOCAL: 驱动程序假定连接时区与 JVM 默认时区相同
SERVER: 驱动程序会尝试从 MySQL 服务器会话变量 'time_zone' 或 'system_time_zone' 上配置的值中检测会话时区
自定义: 必须是JVM 和 MySQL 都支持的时区
不同时区配置的优先级
jvm启动参数 > jvm进程所在服务器时区
jdbc serverTimeZone参数 > mysql本身配置 > mysql所在服务器时区
备注: 以上是2行,没有把5个配置项拿来一起比较,第一行取到的时区,跟第二行取到的时区可以不一样
实验
根据若干实验,对比timestamp与datetime,以及LocalDateTime、Date、ZonedDateTime
实验一
新建一张test表,c_datetime列是datetime类型,c_timestamp是timestamp类型,插入一行数据
修改mysql时区,从GMT+8改成GMT+7,查询test表
实验结论
- datetime不受mysql修改时区影响,效果相当于字符串,存什么查出来就是什么
- timestamp会根据新的时区展示日期和时间
实验二
数据库中有一列datetime类型的列,值为2022-01-01 00:00:00(datetime没有时区属性,相当于字符串)。 Entity用不同的类型接收这一列,结果如下
JVM时区 | jdbc serverTimeZone | String | LocalDateTime | Date | ZonedDateTime |
---|---|---|---|---|---|
GMT+8 | GMT+8 | 2022-01-01 00:00:00 | 2022-01-01T00:00:00 | 2022-01-01T00:00:00+08:00 | 2022-01-01T00:00:00+08:00 |
GMT+8 | GMT+7 | 2022-01-01 00:00:00 | 2022-01-01T00:00:00 | 2022-01-01T00:00:00+07:00 | 2022-01-01T00:00:00+07:00 |
实验结论
- LocalDateTime本身无时区属性,相当于字符串
- 如果Entity用Date或ZonedDateTime接收datetime类型,由于datetime本身无时区属性,要确保在数据存入db后,jdbc serverTimeZone不再修改,否则不同的jdbc serverTimeZone读到的时间值不同。
db是无时区属性的2022-01-01 00:00:00,jdbc serverTimeZone分别为GMT+8和GMT+7时,读到的时间不同
实验三
数据库中有一列timestamp 类型的列,值为2022-01-01T00:00:00+08:00
+08:00表示东八区
用不同的类型接收这一列,结果如下
JVM时区 | jdbc serverTimeZone | String | LocalDateTime | Date | ZonedDateTime |
---|---|---|---|---|---|
GMT+8 | GMT+8 | 2022-01-01 00:00:00 | 2022-01-01T00:00:00 | 2022-01-01T00:00:00+08:00 | 2022-01-01T00:00:00+08:00 |
GMT+8 | GMT+7 | 2022-01-01 00:00:00 | 2022-01-01T00:00:00 | 2022-01-01T00:00:00+07:00 | 2022-01-01T00:00:00+07:00 |
实验结论
- 如果Entity用Date或ZonedDateTime接收timestamp类型,要确保jdbc serverTimeZone与mysql的时区配置一致,否则Entity读到的时间与数据库不一致。
jdbc serverTimeZone=GMT+7时,Entity ZonedDateTime读到的时间是2022-01-01T00:00:00+07:00 ,与db的2022-01-01T00:00:00+08:00差了一个小时
实验四
数据库中有一列timestamp类型的列,Entity通过ZonedDateTime接收。 此时JVM时区、jdbc serverTimeZone、mysql时区都是GMT+8
按以下步骤操作
- 修改mysql时区为GMT+7(global+session)
- insert 一行记录,ZonedDateTime值为2022-01-01T00:00:00+08:00
- timestamp列在GMT+7时区下,值为2022-01-01 00:00:00
问题来了,GMT+7时区下,timestamp列的值,为什么不是2021-12-31 23:00:00 (用Date类型也会有一样的问题)
原因如下:
mysql驱动在发送sql前,会将ZonedDateTime对象参数,根据jdbc serverTimeZone配置的时区转化为日期字符串后,再发送sql请求给mysql server。
同样在mysql server返回查询结果后,结果中的日期值也是日期字符串,mysql驱动会根据jdbc serverTimeZone配置的时区,将日期字符串转化为ZonedDateTime对象。
插入流程如下
此时,执行查询请求,Entity拿到的ZonedDateTime值为2022-01-01T00 :00:00+08:00 ,符合预期(跟插入的时间是一致的),流程如下
接下来,把mysql时区转成GMT+8,跟jdbc serverTimeZone保持一致。
再次执行查询请求(需要重启springboot),最后Entity拿到的ZonedDateTime值为2022-01-01T01 :00:00+08:00 ,不符合预期(跟插入的时间不一致),流程如下
实验结论
jdbc serverTimeZone和mysql时区不一致时,用timestamp类型可能导致数据出错(Entity读到的时间,与最初存入的时间不一致)
总结
timestamp、datetime的对比
- timestamp本身有时区属性,datetime没有时区属性(相当于字符串)
- timestamp本身没有时区问题,只是因为jdbc serverTimeZone和mysql时区不一致,才会有时区问题
- timestamp不允许空值
- timestamp可读性不如datetime
- timestamp最大值是2038-01-19 11:14:07(GMT+8),如果有的业务场景需要存最大值之后的时间(比如房产证有效期截止日),就没法用timestamp
LocalDateTime、Date、ZonedDateTime的对比
- Date/ZonedDateTime有时区属性,LocalDateTime没有时区属性(相当于字符串)
- Entity使用Date/ZonedDateTime时,db到Entity有隐式转换,容易因为修改配置导致bug(参考实验二、实验三)
- ZonedDateTime可以在内存里表示一个非JVM时区的时间,而Date只能表示JVM时区时间,而且ZonedDateTime的API比Date更加灵活易用
强制约束
- jdbc的serverTimeZone参数一定要配
serverTimeZone参数,作用是指定web服务器和mysql服务器的会话期间的mysql服务器时区,即临时指定mysql服务器的时区。 如果没有配置serverTimeZone,那么mysql服务器时区取决于自身配置(配置文件、global级别配置、session级别配置、混乱的CST),容易出bug
- jvm启动参数user.timezone一定要配
如果jvm没配user.timezone,则jvm默认时区取决于操作系统,操作系统的时区非常复杂,好几个文件和环境变量在控制,容易出bug
建议约束
- 数据库时间类型使用没有时区属性的datetime
如果使用timestamp,修改mysql配置会导致数据变化,datetime没有这个问题
- Entity时间类型使用LocalDateTime(LocalDate、LocalTime)或者ZonedDateTime
- jvm启动参数、jvm进程所在服务器时区、jdbc serverTimeZone参数、mysql本身配置、mysql所在服务器时区,这些可以配置时区的地方,全都保持一致
思考与实践
引入时区概念会增加系统复杂度,并非所有时间都需要引入时区。
比如某些单据填报日期的开始、结束日期,是与时区无关的绝对日期,可以不引入时区,降低系统复杂度。
一些涉及状态变更的时区敏感的字段(比如下单时间),需要保证时区的准确性,才能保证系统逻辑的正确性以及用户体验。
以下是一种实践
- 数据库使用无时区属性的datetime
- 遵循"展示与存储分离"的原则,存储时统一为0时区存储,展示时转化为用户当地时区
- Java字段类型,与前端交互使用LocalDateTime,service、dao层使用ZonedDateTime