Java时区与MySQL时区不一致导致的时间不一致问题分析

本文解释了时间戳的概念,探讨了它在不同时区下的表示差异,特别是在Java程序和MySQL数据库中的应用。重点讲解了如何在Java程序中处理时区转换,以及JDBC如何处理时区一致性问题,包括存储和读取操作中的时间戳处理策略。
摘要由CSDN通过智能技术生成

时间戳与时区的关系

时间戳一般指的是Unix 时间戳:是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。

那么和时区又有什么关系呢?

public static void main(String[] args) throws ParseException {
    TimeZone bjTimeZone = TimeZone.getTimeZone("Asia/Shanghai");
    TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");
    
    // 时间戳在不同时区下的日期
    Date date = new Date(0L);
    System.out.println("时间戳 0 对应的系统时间:" + date);
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    sdf.setTimeZone(bjTimeZone);
    System.out.println("时间戳 0 在东八时区下表达的时间:" + sdf.format(date));
    sdf.setTimeZone(utcTimeZone);
    System.out.println("时间戳 0 在UTC时区下表达的时间:" + sdf.format(date));
    
    // 日期在不同时区下的时间戳
    sdf.setTimeZone(bjTimeZone);
    System.out.println("2024-02-25 00:00:00 在东八时区下的时间戳:" + sdf.parse("2024-02-25 00:00:00").getTime());
    sdf.setTimeZone(utcTimeZone);
    System.out.println("2024-02-25 00:00:00 在UTC时区下的时间戳:" + sdf.parse("2024-02-25 00:00:00").getTime());
}

时间戳 0 对应的系统时间:Thu Jan 01 08:00:00 CST 1970
时间戳 0 在东八时区下表达的时间:1970-01-01 08:00:00
时间戳 0 在UTC时区下表达的时间:1970-01-01 00:00:00
2024-02-25 00:00:00 在东八时区下的时间戳:1708790400
2024-02-25 00:00:00 在UTC时区下的时间戳:1708819200

  1. 一个时间戳在不同时区下所表达的时间是不一样的。
  2. 一个日期在不同时区下的时间戳是不同的。东八时区(北京时间)与 UTC 世界协调时间相差了八小时。可以通过时间戳的差值进行计算验证:(1708819200 - 1708790400) / 60 / 60 = 8
  3. 一个日期在不同时区下的时间戳的差值:时区间的偏移量
  4. 两个时区不同,但时间相同的日期表达的意义也不一样,就如北京八点与美国八点的区别

查询、修改 Java 程序使用的时区

public static void main(String[] args) {
 	// 查看默认时区与时区ID
 	TimeZone defaultTimeZone = TimeZone.getDefault();
    ZoneId systemDefaultZoneId = ZoneId.systemDefault();
	// sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=29,lastRule=null]
    System.out.println(defaultTimeZone);
    // Asia/Shanghai
    System.out.println(systemDefaultZoneId);
    
    // 设置默认时区
    TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")));
    // sun.util.calendar.ZoneInfo[id="UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
    System.out.println(TimeZone.getDefault());
}

查询、修改 MySQL 数据库使用的时区

查询:

show global variables like "%time_zone%";
Variable_nameValue
system_time_zone(系统时区)UTC
time_zone(会话时区)SYSTEM

系统时区:UTC,即比东八时区慢8个小时。可以通过 SELECT NOW() 查询当前时间对比 PC 上的时间验证:MYSQL: 2024-02-24 19:07:31 / PC: 2024-02-25 03:07:31。该值读取的就是 MySQL 服务所在的操作系统上使用的时区,以 Linux 系统为例,可通过 date -R 查看:Sat, 24 Feb 2024 19:31:56 +0000。

会话时区:采用系统时区,即 UTC。

修改:
系统时区:修改系统时区,即修改 MySQL 服务所在服务器的时区,以 Linux 操作系统为例:cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime,将时区文件 copy 到 etc 目录下且命名为 localtime。

会话时区:

  1. SQL 的方式:set global time_zone = '+8:00' or set global time_zone = 'Asia/Shanghai'
  2. 修改 my.cnf 配置文件中的 default-time-zone=Asia/Shanghai 属性

JDBC 读取并设置 MySQL 服务使用的时区的流程

mysql-connector-j-8.0.33.jar 为例:

com.mysql.cj.protocol.a.NativeProtocol.configureTimeZone 设置MySQL服务使用的时区的流程:

  1. 优先读取connectionTimeZoneserverTimezone jdbc 属性作为会话使用的时区
  2. 若配置的属性为SERVER,则在第一次调用 ServerSession.getSessionTimeZone() 时读取数据库中配置的时区
  3. 若没有配置则以本地服务器的时区作为 MySQL 服务器的时区
    NativeProtocol.configureTimeZone

JDBC 如何应用的时区

说明:当前Java 程序东八时区,MySQL服务 UTC 时区。

  1. 存储日期数据时,com.mysql.cj.protocol.a.SqlTimestampValueEncoder.getString 对于 Date 类型字段值的处理:将Java程序时区下的日期的时间戳,转为MySQL服务时区下的日期(2024-02-25 11:52:56 > 2024-02-25 03:52:56
    com.mysql.cj.protocol.a.SqlTimestampValueEncoder.getString

  2. 查询日期数据时,com.mysql.cj.result.SqlTimestampValueFactory.localCreateFromDatetime 对于 Date 类型字段的处理:将MySQL服务时区下的日期的时间戳,转为Java程序时区下的日期。(2024-02-24 21:34:55 > 2024-02-25 05:34:55
    com.mysql.cj.result.SqlTimestampValueFactory.localCreateFromDatetime

根据源代码的实现可以发现一个规律:都是先将日期根据所属时区转换为时间戳后,在根据需要转换的时区转换为最终日期。

Java 程序时区与 MySQL 服务使用时区不一致导致的问题

JDBC 读取并设置 MySQL 服务使用的时区的流程 中说到:MySQL 服务使用的时区会受到 jdbc 参数的影响,也就是说可能会出现:实际的数据库时区与 jdbc 参数声明的时区是不一样的。最坏情况下会出现:Java 程序时区、jdbc 声明时区、实际数据库时区都是不同的。

不对每种情况的流程进行逐个分析。一般开发时遇到时间不一致时,大概都是分为以下的两种情况:

  1. Java 程序时区 与 jdbc 参数时区一致,实际数据库时区不一致:这种情况下,会出现程序、数据库中展示日期都是相同的,但两个日期分别对应的时间戳不相同。看起来是没有问题,但实际上,数据库中存储的日期的时间戳已经不是Java程序中该日期所对应的时间戳了。如最开始说到的:一个日期在不同时区下的时间戳是不同的,那么表达的意义也不一样,就如北京八点与美国八点的区别。整理一下 JDBC 驱动包在这种情况下的处理流程:
    1)存储:

    • 获取日期在 Java 程序时区下的时间戳
    • 根据 JDBC 参数时区将时间戳转为 MySQL 服务时区下的日期字符串(由于二者一致,所以结果没有变化)
    • 组装为数据包发送给实际的 MySQL 服务
    • MySQL 服务根据该日期字符串,转为实际数据库时区下的该日期(此时时间戳已经不同了)

    2)读取(能够在转为 Java 程序中的正确日期):

    • 读取实际数据库中的日期字符串
    • 根据 JDBC 参数时区将日期转为对应时间戳
    • 将时间戳在根据 Java 程序时区转为对应的日期(由于与 JDBC 参数时区一致,最终程序中的日期还是相同的)
  2. Java 程序时区 与 实际数据库时区不一致:这种情况下不考虑 jdbc 参数时区的影响,会出现程序、数据库中展示的日期不同,但两个不同日期的时间戳是一致的。捋一下 JDBC 处理流程:
    1)存储:

    • 获取日期在 Java 程序时区下的时间戳
    • 获取实际 MySQL 服务的时区,并转为该时区下的日期字符串(由于二者不一致,所以转换结果日期也不同)
    • 组装为数据包发送给实际的 MySQL 服务
    • MySQL 服务根据该日期字符串,转为实际数据库时区下的该日期(此时展示的日期已经不同,但时间戳还是相同的)
      2)读取(能够在转为 Java 程序中的正确日期):
    • 读取实际数据库中的日期字符串
    • 获取实际 MySQL 服务的时区,将日期转为对应时间戳
    • 将时间戳在根据 Java 程序时区转为对应的日期(虽然时区不同,但是时间戳还是同一个,能够在正确转为 Java 程序中的正确日期)
  3. 仅依靠数据库时区生成时间数据(这是一种特殊情况):
    当我们在为创建、修改时间字段添加了以下自动获取时间属性时:

    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间'
    

    1)存储:仅依赖数据库时区,不涉及时区转换的问题
    2)读取(能够在转为 Java 程序中的正确日期):

    • 读取实际数据库中的日期字符串
    • 获取实际 MySQL 服务的时区,将日期转为对应时间戳
    • 将时间戳在根据 Java 程序时区转为对应的日期(虽然时区不同,但是时间戳没有发生变化,能够在正确转为 Java 程序中的正确日期)

解决方式

  1. 将 Java 程序时区、jdbc 声明的参数时区、实际数据库时区设置为相同的时区
  2. 将 Java 程序时区、实际数据库时区设置为相同的时区,且不设置 jdbc 声明的参数时区这一干扰配置
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值