JDBC MySQL 时区问题

以下所有讨论都是基于 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'

关键参数:

  1. serverTimezone=UTC
  2. sessionVariables=character_set_connection=utf8mb4,character_set_client=utf8mb4,time_zone='%2b00%3a00'

如果 serverTimezonetime_zone 参数值不一致,就会触发上述问题。

原理分析

时区相关的参数


JRE

JRE 在运行时的默认时区,以 jre_default_timezone 表示,可以调用以下方法获取。

java.util.TimeZone#getDefault()

如果不对以上参数做任务显式的设置,以上方法获取到的就是操作系统的时区。

改变以上参数的方法有两种:

  1. 通过指定jvm参数设置 user.timezone
    -Duser.timezone=UTC
    
  2. 设置操作系统环境变量
    export TZ=UTC
    

JDBC

serverTimezone

该参数用于指定 MySQL 服务器当前使用的时区,可以不显式的指定,默认会尝试从 MySQL 服务器获取。
之所以要显式指定该参数,是因为如果从 MySQL 服务器获取到的时区表示方式是 Java 不支持的格式,为了避免两边使用时区不一致的错误,就会抛出异常。

System.out.println(TimeZone.getTimeZone("不支持的ZoneID").getID());
// 以上并不会报错,而是会输出 GMT ,也是就 UTC

MySQL 服务器当前使用的时区会用在:

  1. java.sql.Datejava.sql.Timejava.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)
    
  2. 将查询结果解析成 java.sql.Datejava.sql.Timejava.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
image

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:11UTC --> +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:11UTC --> +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 日期相关的类型还有:

  1. DATE
  2. DATETIME

TIMESTAMP 在存储时,会将时间戳从当前时区(time_zone参数值)转换成 UTC 进行存储,在读取时,会将时间戳从 UTC 时区转换为当前时区。
但是 DATEDATETIME 在存取时并不会做上面的转换,而是将字面值直接存取,所以在这面的表格中没有写出 DATEDATETIME

从上面的时区转换过程可以看出,在 MySQLJDBC 实现中,如果 serverTimezonejre_default_timezone 不一致,那么 LocalDateTimeLocalDate 的查询结果就会和插入时不一样。

20200514182000

以上图例中,MySQL 数据类型使用 TIMESTAMP , Java 程序中数据类型使用 java.util.Date 。

结论

为了避免时区问题的发生,应保证 serverTimezonesession.time_zone 保持一致,同时尽量不要在 Java 程序中使用 LocalDateTimeLocalDate 来作为查询参数或接收查询结果。

另外在做表结构设计时,也最好使用 TIMESTAMP 类型。

你可能感兴趣的:(JDBC MySQL 时区问题)