前端→Java→MySQL 时区时间处理全链路深度解析

1、简介

在系统开发的时候, 如果时区时间的使用错误,常常会造成一些不符合预期的结果。 尤其在一些全球化部署的系统中,需要额外注意,今天主要讲讲在使用时区时间的时候我们需要注意什么, 比如后端是如何处理前端上送的时间的, 还有 new Date() 到底是什么, 和 LocalDateTime.now()有什么区别。 JDBC连接协议的serverTimezone有什么用, 跟mysql服务器本身的时区又是什么有什么关系

2、基础概念

2.1、时区 和 UTC时间

由于时间是由人定义的,而且是根据人们看到太阳的时间去定义什么是早上、什么是晚上, 你老家这里是早上, 但是别的地区那里可能是晚上, 所以为了统一时间标准, 定义了一个全球标准时间, 这个时间也叫UTC时间, 全称叫 协调世界时UTC, Coordinated Universal Time)。然后全球其他地区都以它为参照, 去定义你比它快多少小时, 或者说你比它慢多少小时。

然后全球总共被划分为24个时区,而UTC又叫零时区时间, 如果比它大一个1小时就是叫东1区时间,比它小一个小时的时间就叫西1区时间, 像我们的北京时间就叫东八区时间,比它大了8个小时。

在开发时一些系统我们还常常能看到 GMT时间,全称叫格林尼治标准时间"(GMT,基于天文观测), 可以认为与UTC一样,也是标准时间, 但是精度没有UTC高, 在写代码、配服务器、存数据库时,请统一使用 UTC。GMT 更多是历史遗留和民用习惯。

2.2、时间戳

时间戳(Unix Timestamp)表示自 1970-01-01T00:00:00Z 起经过的秒数或毫秒数

  • 为什么是1970-01-01T00:00:00Z这个时间? Unix 系统诞生时随意定的,后来成了全球标准

例子

  • 时间戳 0 = 1970-01-01 00:00:00 UTC (起点)
  • 时间戳 1 = 1970-01-01 00:00:01 UTC (1秒后)
  • 时间戳 2 = 1970-01-01 00:00:02 UTC (2秒后)
  • 时间戳 86400 = 1970-01-02 00:00:00 UTC (1天后)

像Java里的System.currentTimeMillis() 就是获取的当前UTC时间点 距离 1970-01-01 00:00:00 经过的毫秒数, 底层是OS操作系统内部有一个计数器直接返回即可, 而不是什么先获取当前时间又去减掉1970-01-01 00:00:00计算出来。

2.3、夏令时规则 (DST)

什么是夏令时? (DST - Daylight Saving Time)

夏令时是一种为了节约能源,在夏季将时钟人为拨快 1 小时的制度。

  • 春天:时钟拨快 1 小时(少睡 1 小时,例如从 02:00 直接跳到 03:00)。
  • 秋天:时钟拨回 1 小时(多睡 1 小时,例如从 02:00 跳回 01:00)。

哪些国家有?

  • :美国、加拿大、大部分欧洲国家。
  • 中国(1991 年后已取消)、日本、印度、韩国等。

2.4、ISO8601标准时间格式(重点)

知道了时区之后, 我们再来讲讲ISO8601时间标准, 它是一种国际标准的日期和时间表示法,目的是为了提供一种清晰、无歧义、全球统一的日期和时间格式,以便于计算机系统处理、数据交换以及跨国交流,消除因不同地区日期格式习惯(如美式 MM/DD/YYYY 与欧式 DD/MM/YYYY)不同而产生的混淆。

完整的ISO时间结构为

多说无益, 直接让我们去看看下面几个时间格式到底表示什么

1、 "2026-03-21T21:37:00Z"

  • 可以看到和我们常见的时间 "2026-03-21 21:37:00" 区别就是 多了个T 和最后面多了个Z。
  • 这个 T 其实没什么特别意思, 主要是为了区分日期和时间的部分的分隔符,跟空格也是一样的作用,只是因为空格在 URL、某些协议里容易引起歧义,而T 更明确,机器解析也更方便。
  • 然后这个Z表示的是时区信息, 用于这个时间是属于哪个地区的时间。 而字母Z就表示的零时区, 也就说这个时间是我们的UTC时间。 你肯定想那表示北京时间的字母是啥, 很遗憾这个并没有。ISO 8601 标准中只有 Z 一个字母(代表 UTC+0),没有为其他时区定义字母。 他们是另外的表示方法,具体看下面

2、 "2026-03-21T21:37:00+08:00"

  • 可以看到最后多了个 "+08:00" 表示, 这个意思是这个时间2026-03-21T21:37:00比UTC时间大了8个小时, 2026-03-21T21:37:00也就是我们的东八区时间北京时间。 所以对应的UTC时间就是 2026-03-21T21:37:00 - 08:00 = "2026-03-21T13:37:00Z
  • 同理如果是 "+02:00"就表达比UTC大2个小时, "+00:00"就表示这就是UTC时间,等同于字母Z。 如果是 "-08:00" 就表示比UTC时间小了8个小时. 比如 "2026-03-21T21:37:00-08:00" 则 对应的UTC时间就是 "2026-03-21T21:37:00" + 08:00 = 2026-03-22T05:37:00

常见的ISO标准时间格式

格式 示例
日期 2024-03-25
日期+时间 2024-03-25T10:30:00
含毫秒 2024-03-25T10:30:00.123
含时区偏移 2024-03-25T10:30:00+08:00
UTC(Z表示+00:00) 2024-03-25T10:30:00Z
含时区名称 2024-03-25T10:30:00+08:00[Asia/Shanghai]

像我们日常见到的时间 "2026-03-21 21:37:00" 是没有带时区信息,也不符合ISO标准时间格式, 如果在开发的时候需要额外注意,这个时间来源和目的, 而且一般需要在接口手动使用**@JsonFormat** 或者 @DateTimeFormat 去解析,否则会解析失败,因为Spring Controller接口的时间字段一般默认只支持对ISO标准时间格式或者时间戳进行解析

3、Java里的时间对象

Date:

  • 就是一个时间戳,它内部存储的是一个 long 值(自 1970-01-01 00:00:00 UTC 以来的毫秒数)。 new Date()等同于调用System.currentTimeMillis();

  • Date的toString() 在不同时区打印会不一样, 会先获取系统时区,然后根据该时区将时间戳转成ISO标准时间进行输出

  • 精度 :只精确到毫秒

  • Java 1.0 就有的类,

Instant

  • 也是一个时间戳。只是精度精确到纳秒, 就是的 自"1970-01-01 00:00:00 UTC" 之后的秒数 + 纳秒数
  • 类似于 Date 的现代版,但更纯粹。与Date区别就是
    • API更丰富、更新
    • 值是不可变的,线程安全的
    • 精度更精
  • Instant的toString() 打印的是UTC时间,与系统时区无关

LocalDateTime

  • 表示没有时区信息的日期和时间,比如"2024-03-25 10:30:00"

  • 不能直接转换为时间戳(因为不知道时区,无法计算相对于 UTC 的偏移)。

  • LocalDateTime.now() 获取的时间到底是怎么来的,它的获取逻辑如下

    • ↓ 获取当前系统时间戳

    • ↓获取JVM默认时区

    • ↓ 将该时间戳 转换成 该时区的时间 ,最终得到年月日时分秒,然后去掉时区信息

ZoneOffset:

  • 就是 时间偏移量", 就是为了表示我们 ISO标准时间的 +08:00 (比 UTC 快 8 小时) 、-05:00 (比 UTC 慢 5 小时)这种时间偏移量

OffsetDateTime

  • 就是带"时间偏移量"的时间
  • 组成结构: "2024-03-25 10:30:00+08:00"
  • 可以认为 OffsetDateTime = LocalDateTime (本地日期时间) + ZoneOffset (固定偏移量)
  • 无法自动处理夏令时
    • 假设某地区标准偏移量是 +05:00,夏令时后应该变成 +06:00,但 OffsetDateTime 的偏移量仍然固定是 +05:00,不会自动调整。

代码示例

java 复制代码
LocalDateTime ldt = LocalDateTime.of(2024, 3, 25, 10, 30);
ZoneOffset offset = ZoneOffset.of("+08:00");

// 组合成 OffsetDateTime
OffsetDateTime odt = ldt.atOffset(offset); 

// 结果:2024-03-25T10:30+08:00

ZonedDateTime:

  • 可以认为是 LocalDateTime + ZoneId(完整时区,如 Asia/Shanghai)。
  • 组成结构: "2024-03-25T10:30:00+08:00[Asia/Shanghai]"
  • 这个由于知道时区信息(ZoneId), 不仅知道偏移量,还知道夏令时(DST)规则。例如,当时间跨越夏令时切换点时,它能自动调整时间.

JAVA时间对象总结

可以看到,对于各种不同的时间格式字符串, Java定义了各种对象去表现这些时间, 为了方便快速记忆每个时间对象的特点,整理下面表格

时间对象 代表的时间格式 前端上送时默认支持处理的格式
Date 1700000000000 时间戳 或者 2024-01-15T10:30:00+08:00
Instant 1700000000000 时间戳 或者 2024-01-15T10:30:00+08:00
LocalDateTime 2024-01-15T10:30:00 2024-01-15T10:30:00
ZonedDateTime 2024-01-15T10:30:00+08:00[Asia/Shanghai] 所有ISO标准时间格式
OffsetDateTime 2024-01-15T10:30:00+08:00 2024-01-15T10:30:00+08:00
ZoneOffset +08:00 +08:00

下面一张图快速快速记忆他们的关系和区别

4、时间处理梳理链路分析

4.1、前端到JAVA

一般从前端到JAVA, 前端先上送时间(可能各种格式时间戳、ISO标准时间),然后后端接收到这个时间字符串后 根据Controller接口的时间对象字段进行反序列化。

其中我们用的比较多的就是Jackson的@JsonFormat 注解去配置如何对时间进行解读。 因为默认是只支持对时间戳或者ISO时间格式做处理,但是我们大部分前端上送的都是 "2024-01-09 15:30:00" 这种不符合的格式,所以需要使用该注解。

接下来会结合@JsonFormat注解的使用重点讲述即这个从前端时间字符串 是如何 映射到Java时间对象的。当然即使该字段没配@JsonFormat注解,整体处理流程也是类似的,只是使用一些默认值去处理而不是用@JsonFormat 注解指定的,大家举一反三即可。

@JsonFormat 注解处理流程

这个注解作用主要配置怎么从时间字符串 Java时间对象的转换过程,或者说序列化反序列化过程. 其中pattern属性表示是什么格式,方便我们去解析处理。 而timezone属性则告诉 Jackson "这个没有时区信息的时间字符串,应该被当作哪个时区来解释"。

  • 当然如果你没配置timezone这个属性,也会去取默认值
  • 默认值优先级是
    • 第一)字段上 @JsonFormat(timezone=)
    • 第二) 全局配置 spring.jackson.time-zone
    • 第三)JVM 默认时区(-Duser.timezone=...)
    • 第四)操作系统时区
Date对象转换

假设有以下字段

java 复制代码
 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
 private Date createTime;

1、反序列化过程(JSON → Java), 从时间字符串 到 Date的 具体处理过程

有请求JSON: { "createTime": "2024-03-25 10:30:00" }

Jackson 解析"2024-03-25 10:30:00"的步骤:

  • ↓ 按 pattern 解析为时间数字
  • ↓ 发现字符串无时区信息
  • ↓ 用 timezone="Asia/Shanghai" 补充 → 10:30:00 +08:00
  • ↓ 转换为 UTC 时间→ 02:30:00 UTC
  • ↓ 再将UTC 时间转为时间戳 1711330200000
  • ↓ 再将时间戳1711330200000 存入 Date内部

2、序列化过程( Java → JSON ), 从Date到时间字符串的具体处理过程

Jackson 将Date序列化的步骤:

  • ↓ Date 内部 UTC 毫秒数 是1711330200000
  • ↓ 时间戳转换为 UTC 时间→ 02:30:00 UTC
  • ↓ 按 timezone="Asia/Shanghai" 换算时区 → 10:30:00 +08:00
  • ↓ 按 pattern 格式化
  • ↓ 输出 "2024-03-25 10:30:00"
LocalDateTime对象转换

假设有以下字段

java 复制代码
  @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
  private LocalDateTime createTime;

1、反序列化过程(JSON → Java), 从时间字符串 到 LocalDateTime的 具体处理过程

有请求JSON: { "createTime": "2024-03-25 10:30:00" }

Jackson 解析"2024-03-25 10:30:00"的步骤:

  • ↓ 按 pattern 解析为时间数字

  • ↓ LocalDateTime 本身无时区概念,timezone 属性直接忽略

  • ↓ 字面是什么就存什么, 所以 LocalDateTime = 2024-03-25T10:30:00(无时区)

2、序列化过程( Java → JSON ), 从LocalDateTime 到 时间字符串 的 具体处理过程

Jackson 将LocalDateTime序列化的步骤:

  • ↓ 假设有LocalDateTime = 2024-03-25T10:30:00
  • ↓ 按 pattern 直接格式化字面值
  • ↓ timezone 属性同样忽略,不做时区换算
  • ↓ 输出 "2024-03-25 10:30:00"

可以看到与Date最大区别是没什么弯弯绕绕和转换, timezone字段也是不生效的, 你字面传什么就是什么

ZonedDateTime对象转换

假设有以下字段

java 复制代码
  @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
  private ZonedDateTime createTime;

1、反序列化过程(JSON → Java), 从时间字符串 到 ZonedDateTime的 具体处理过程

情况一:输入字符串自带时区信息

有请求JSON: { "createTime": "2024-03-25T10:30:00+05:30" }

Jackson 解析"2024-03-25T10:30:00+05:30"的步骤:

  • ↓ 字符串本身携带时区 +05:30(印度时区)
  • ↓ @JsonFormat 的 timezone="Asia/Shanghai" 被忽略
  • ↓ 直接按字符串时区解析
  • ↓ 获取时间偏移量+05:30对应的时区名称为 [Asia/Kolkata]
  • ↓ 最后组装 ZonedDateTime = 2024-03-25T10:30:00+05:30[Asia/Kolkata]

情况二:输入字符串不带时区信息

有请求JSON: { "createTime": "2024-03-25 10:30:00" }

Jackson 解析"2024-03-25 10:30:00"的步骤:

  • ↓ 按 pattern 解析字面值,发现无时区信息
  • ↓ 用 timezone="Asia/Shanghai" 补充时区
  • ↓ 最后组装 ZonedDateTime = 2024-03-25T10:30:00+08:00[Asia/Shanghai]

2、序列化过程( Java → JSON ), 从ZonedDateTime 到 时间字符串 的 具体处理过程

Jackson 将ZonedDateTime序列化的步骤:

  • ↓ 假设有 ZonedDateTime = 2024-03-25T10:30:00+08:00[Asia/Shanghai]
  • ↓ 如果此时的 注解配的时@JsonFormat(timezone="UTC"), 发现和ZonedDateTime本身的时区信息不一样
  • ↓ 就换算到 UTC → 02:30:00 UTC
  • ↓ 最后按 pattern 格式化输出 "2024-03-25 02:30:00"

4.2、Java到MYSQL的读取写入

就是写入到MYSQL时, Java时间对象 是怎么转换成 时间字符串发给 mysql的, 然后mysql是怎么解读并存储到列的

还有读取时mysql是怎么从列读取出来变成 时间字符串 再返回给Java反序列化成时间对象的,接下来详细分析。

4.2.1 MYSQL时区体系

1、先搞清楚 MySQL 的时区

执行下面命令可以查看mysql的时区设置

sql 复制代码
SHOW VARIABLES LIKE '%time_zone%';

system_time_zone: mysql所属的操作系统时区,CST就是东八区

time_zone(关键): MySQL全局/会话的时区偏移量, +00表示UTC零时区, +08表示东八区

2、mysql的时区的作用

  • 对当前时间函数的影响,时间函数获取的是time_zone时区的时间

    sql 复制代码
    NOW()                -- 当前日期时间
    CURRENT_TIMESTAMP()  -- 同 NOW()
    SYSDATE()            -- 同 NOW()
    CURDATE()            -- 当前日期
    CURTIME()            -- 当前时间
  • TIMESTAMP类型字段的存储会进行转换

    • 我们知道 TIMESTAMP 内部存储的就是时间戳类型,但是 MySQL 不会直接把时间戳数字给你而是会在读写的时候根据当前的time_zone换成成对应的时间字符串

      • 写入时:MySQL 会将当前会话(Session)的时区时间转换为 UTC时间戳 存储。
      • 读取时:MySQL 会将存储的 UTC时间戳转换回当前会话的时区时间显示。
    • TIMESTAMP 列的默认值和自动更新 也会受影响

      sql 复制代码
      CREATE TABLE t (
        id INT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,     -- 受时区影响
        updated_at TIMESTAMP ON UPDATE CURRENT_TIMESTAMP    -- 受时区影响
      );
  • 对 UNIX_TIMESTAMP() 和 FROM_UNIXTIME() 函数的影响

    • FROM_UNIXTIME函数: 时间戳转成"当前时区"的时间字符串
    • UNIX_TIMESTAMP函数: 把字符串按"当前时区"解读,转成时间戳
  • 一些间接影响

    • 主从复制时, 主库和从库时区设置不一样时,在statement 复制模式同步下, 如果SQL包含一些时间函数,则同步后的时间会不一样

3、那对MYSQL的其他时间字段DATETIME、DATE、TIME、YEAR是否有影响?

  • 不会,这些字段存什么就是什么不会做转换

4.2.2 再看JDBC驱动serverTimezone配置

properties 复制代码
spring.datasource.url=jdbc:mysql://localhost:3306/db?serverTimezone=Asia/Shanghai 

一般我们的JDBC连接协议都会配一个serverTimezone,它的作用主要是 告诉 JDBC 驱动在"翻译"Java时间对象 和 MySQL字符串 之间转换时用哪个时区去解读这个时间。接下来会从读取和写入MYSQL时间的过程详细分析。

4.2.3 读取过程

我们 假设当前条件如下, 模拟从MySQL --》Java的读取流程

  • JDBC的serverTimezone 为 America/New_York(UTC-5)纽约时间
  • MySQL 的 time_zone = Asia/Shanghai(UTC+8)上海时间
  • 数据库此时存储的值
    • DATETIME 列 = "2024-01-15 17:30:00"(上海时间字面值)
    • TIMESTAMP 列 = 1705311000ms, (对应的UTC时间为 2024-01-15 09:30:00Z).

从上面可以看到serverTimezone作用主要是Java在收到MYSQL的时间字符串后, 由于该时间字符串没有携带时区信息,但是映射成OffsetDateTime对象和Date对象都要时区信息才能转换, 所以默认就用配置的serverTimezone时区去解读这个时间。而LocalDateTime这种类型本身不需要时区信息,所以直接原样映射即可。

还有一点值得注意是其实MYSQL的TIMESTAMP 列存储的是时间戳, 但是奇怪的是我们去查的时候去看的时候确实一个完整的时间,这是因为,在读取时MYSQL会自动根据当前时区将当前时间戳换算成时间格式字符串。

4.2.4写入过程

我们 假设当前条件如下, 模拟从Java --》MySQL的写入流程

  • JDBC的serverTimezone 为 UTC
  • MySQL 的 time_zone = Asia/Shanghai(UTC+8)
  • 当前Java时间对象的时间是 北京时间 2024-01-15 17:30:00
    • 对应的 UTC时间是 2024-01-15 09:30:00
    • 对应的时间戳是 1705308600000ms

1、可以看到MYSQL时区的配置主要是负责对TIMESTAMP类型字段进行时间戳和时间字符串的转换, 而其他类型如DATETIME则没有作用,而是客户端发什么,它就存什么

2、JDBC的serverTimezone理解

  • 可以看到JDBC连接协议的serverTimezone真正作用 是 在 Java时间戳(绝对时刻) 和 MySQL时间字符串(字面值) 之间互转时 告诉 JDBC 驱动用哪个时区来格式化/解析, 像一个"翻译器", 你给它 输入一个绝对时间点(时间戳), 它给你输出一个该时区对应的时间字符串。 所以对于LocalDateTime这种没有时区信息也就没有绝对时刻的东西就无法完成转换,这JDBC就原样发给MYSQL不做转换。

就像你问:

"北京时间17:30,换成UTC是几点?" → 能算,09:30。
"17:30,换成UTC是几点?" → 不知道,17:30是哪里的?没法算

5、总结

我们整个时间处理链路其实就是如下图

需要注意点就是

1、JDBC连接协议的serverTimezone配置需要和MYSQL服务的时区保持一致,否则可能有一些不符合预期的时间, 更推荐都配成UTC,这样无论哪个地区都能兼容, 甚至避免夏令时等边界问题

2、MYSQL的TIMESTAMP字段类型在处理上与其他所有时间类型字段(DATE、TIME、DATETIME)都不同, 一般业务时间字段推荐用 DATETIME, 需要记录绝对时刻(如日志、审计)用 TIMESTAMP 或直接存 BIGINT 时间戳。

3、MYSQL使用一些时间函数 select now()啥的一定要注意当前MYSQL的time_zone,否则SQL过滤时可能是错误。 比如你用的是DATETIME字段存的是北京时间,但是你myql时区又是UTC则now()出来的是UTC时间, 则 一些什么 select DATETIME字段 > now() 就不符合预期

4、默认Spring只支持处理前端上送的时间戳或者 ISO标准的时间字符串, 这样才能自动进行反序列化到什么Date、LocalDateTime上面, 你用我们日常生活常见的什么 "2024-01-15 22:30:00"格式则需要做一些配置去映射,否则会抛反序列化异常。

5、java的JVM也有默认的时区配置, 默认是继承自操作系统,也可以通过JVM 启动参数:-Duser.timezone去指定, 然后 JAVA里面的一些时间对象就会根据这个值去自动转换。 比如各种 xx.now() 就是获取该时区的当前时间。

相关推荐
踩着两条虫2 小时前
AI驱动的 Vue3应用开发平台深入探究(十五):扩展与定制之自定义设置器与属性编辑器
前端·vue.js·人工智能·低代码·系统架构·编辑器
壹方秘境2 小时前
作为开发者,我们需要的可能不是Wireshark那样的数据包分析工具,也不是Stream、ProxyPin那样的抓包工具
后端·ios
恋猫de小郭2 小时前
Flutter 3.41.6 版本很重要,你大概率需要更新一下
android·前端·flutter
Surmon7 小时前
彻底搞懂大模型 Temperature、Top-p、Top-k 的区别!
前端·人工智能
木斯佳10 小时前
前端八股文面经大全:bilibili生态技术方向二面 (2026-03-25)·面经深度解析
前端·ai·ssd·sse·rag
不会写DN10 小时前
Gin 日志体系详解
前端·javascript·gin
冬夜戏雪10 小时前
实习面经记录(十)
java·前端·javascript
zb2006412011 小时前
CVE-2024-38819:Spring 框架路径遍历 PoC 漏洞复现
java·后端·spring
uzong11 小时前
AI Agent 是什么,如何理解它,未来挑战和思考
人工智能·后端·架构