以下所有讨论都是基于 MySQL 数据库
问题描述
在用 Java 做数据库开发时,有时会碰到 Java 程序中的时间与保存到数据库中的时间不一致的问题。
先给出解决方案
jdbc:mysql://${spring.datasource.mosaic.host}:${spring.datasource.mosaic.port}/${spring.datasource.mosaic.database}?serverTimezone=UTC
&useAffectedRows=true&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&connectTimeout=2000
&sessionVariables=character_set_connection=utf8mb4,character_set_client=utf8mb4,time_zone='%2b00%3a00'
关键参数:
serverTimezone=UTC
- sessionVariables=character_set_connection=utf8mb4,character_set_client=utf8mb4,
time_zone='%2b00%3a00'
如果 serverTimezone
和 time_zone
参数值不一致,就会触发上述问题。
原理分析
时区相关的参数
JRE
JRE
在运行时的默认时区,以 jre_default_timezone
表示,可以调用以下方法获取。
java.util.TimeZone#getDefault()
如果不对以上参数做任务显式的设置,以上方法获取到的就是操作系统的时区。
改变以上参数的方法有两种:
- 通过指定jvm参数设置
user.timezone
-Duser.timezone=UTC
- 设置操作系统环境变量
export TZ=UTC
JDBC
serverTimezone
该参数用于指定 MySQL 服务器当前使用的时区,可以不显式的指定,默认会尝试从 MySQL 服务器获取。
之所以要显式指定该参数,是因为如果从 MySQL 服务器获取到的时区表示方式是 Java 不支持的格式,为了避免两边使用时区不一致的错误,就会抛出异常。
System.out.println(TimeZone.getTimeZone("不支持的ZoneID").getID());
// 以上并不会报错,而是会输出 GMT ,也是就 UTC
MySQL 服务器当前使用的时区会用在:
-
java.sql.Date
、java.sql.Time
、java.sql.Timestamp
的格式化后用于拼装SQL
// 源码参考 com.mysql.cj.ClientPreparedQueryBindings#setTimestamp(int, java.sql.Timestamp, java.util.Calendar, int) com.mysql.cj.ClientPreparedQueryBindings#setDate(int, java.sql.Date, java.util.Calendar) com.mysql.cj.ClientPreparedQueryBindings#setTime(int, java.sql.Time, java.util.Calendar)
- 将查询结果解析成
java.sql.Date
、java.sql.Time
、java.sql.Timestamp
// 源码参考 com.mysql.cj.jdbc.result.ResultSetImpl#getTimestamp(int, java.util.Calendar) com.mysql.cj.jdbc.result.ResultSetImpl#getTime(int, java.util.Calendar) com.mysql.cj.jdbc.result.ResultSetImpl#getDate(int, java.util.Calendar)
时区配置源码参考:
com.mysql.cj.protocol.a.NativeProtocol#configureTimezone
MySQL
show variables like '%time_zone%';
Variable_name | Value |
---|---|
system_time_zone | UTC |
time_zone | SYSTEM |
system_time_zone
是指运行 MySQL 服务的服务器时区,要改变该参数值,通常做法是用环境变量 TZ
来指定。
time_zone
是 MySQL 处理需要用到时区信息的数据时所使用的时区,如果不显式指定,默认继承自 system_time_zone
,在通过 JDBC 连接 MySQL 时,可以通过设置会话参数来设置该参数的值。
假定当前时间是 UTC 2020-04-01 00:00:00
set @@time_zone = '+08:00';
select current_timestamp();
current_timestamp() | current_date() | current_time() |
---|---|---|
2020-04-01 08:00:00 | 2020-04-01 | 08:00:00 |
set @@time_zone = '+08:00';
insert into test_table (id, create_timestamp) values (1, '2020-04-08 16:32:11');
如果 create_timestamp
数据类型是 TIMESTAMP
的话,则实际存储的值是 2020-04-08 08:32:11
(+08:00
--> UTC
)
如果 create_timestamp
数据类型是 DATETIME
的话,则实际存储的值是 2020-04-08 16:32:11
(原样存储,不做任何转换)
set @@time_zone = '+08:00';
select create_timestamp from test_table where id=1;
如果 create_timestamp
数据类型是 TIMESTAMP
的话,则返回 2020-04-08 16:32:11
(UTC
--> +08:00
)
如果 create_timestamp
数据类型是 DATETIME
的话,则返回 2020-04-08 16:32:11
(原样返回,不做任何转换)
set @@time_zone = '+07:00';
select create_timestamp from test_table where id=1;
如果 create_timestamp
数据类型是 TIMESTAMP
的话,则返回 2020-04-08 15:32:11
(UTC
--> +07:00
)
如果 create_timestamp
数据类型是 DATETIME
的话,则返回 2020-04-08 16:32:11
(原样返回,不做任何转换),此时就已经不能正确表示原始时间点了
日期相关的数据类型转换
由 Java 程序到 MySQL
Java | 右转时区 | JDBC | 右转时区 | SQL | 右转时区 | MySQL |
---|---|---|---|---|---|---|
java.util.Date | jre_default_timezone | java.sql.Date | serverTimezone | yyyy-MM-dd HH:mm:ss | session.time_zone --> UTC | TIMESTAMP |
java.time.Instant | jre_default_timezone | java.sql.Timestamp | serverTimezone | yyyy-MM-dd HH:mm:ss | session.time_zone --> UTC | TIMESTAMP |
java.time.LocalDateTime | jre_default_timezone | java.sql.Timestamp | serverTimezone | yyyy-MM-dd HH:mm:ss | session.time_zone --> UTC | TIMESTAMP |
java.time.LocalDate | jre_default_timezone | java.sql.Date | serverTimezone | yyyy-MM-dd | session.time_zone --> UTC | TIMESTAMP |
由 MySQL 程序到 Java
MySQL | 右转时区 | SQL | 右转时区 | JDBC | 右转时区 | Java |
---|---|---|---|---|---|---|
TIMESTAMP | UTC --> session.time_zone | yyyy-MM-dd HH:mm:ss | serverTimezone | java.sql.Timestamp | jre_default_timezone | java.util.Date |
TIMESTAMP | UTC --> session.time_zone | yyyy-MM-dd HH:mm:ss | serverTimezone | java.sql.Timestamp HH:mm:ss | jre_default_timezone | java.time.Instant |
TIMESTAMP | UTC --> session.time_zone | yyyy-MM-dd HH:mm:ss | java.time.LocalDateTime | |||
TIMESTAMP | UTC --> session.time_zone | yyyy-MM-dd | java.time.LocalDate |
MySQL 日期相关的类型还有:
- DATE
- DATETIME
TIMESTAMP
在存储时,会将时间戳从当前时区(time_zone
参数值)转换成 UTC
进行存储,在读取时,会将时间戳从 UTC
时区转换为当前时区。
但是 DATE
和 DATETIME
在存取时并不会做上面的转换,而是将字面值直接存取,所以在这面的表格中没有写出 DATE
和 DATETIME
。
从上面的时区转换过程可以看出,在 MySQL
的 JDBC
实现中,如果 serverTimezone
和 jre_default_timezone
不一致,那么 LocalDateTime
和 LocalDate
的查询结果就会和插入时不一样。
以上图例中,MySQL 数据类型使用 TIMESTAMP , Java 程序中数据类型使用 java.util.Date 。
结论
为了避免时区问题的发生,应保证 serverTimezone
和 session.time_zone
保持一致,同时尽量不要在 Java 程序中使用 LocalDateTime
和 LocalDate
来作为查询参数或接收查询结果。
另外在做表结构设计时,也最好使用 TIMESTAMP
类型。