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 类型。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,198评论 6 514
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,334评论 3 398
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,643评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,495评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,502评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,156评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,743评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,659评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,200评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,282评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,424评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,107评论 5 349
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,789评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,264评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,390评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,798评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,435评论 2 359