本文大纲

ARTICLE / 技术文章

MySQL DATETIME 没变,为什么 Java 里差了八小时

Java与数据库 · 2026/9/27

MySQL DATETIME 没变,为什么 Java 里差了八小时

排查 Java 与 MySQL 的时间问题时,我最先问的不是“服务器设成什么时区”,而是:**数据库列是什么类型?Java 读成什么类型?应用想保存一个确定瞬间,还是只想保存墙上显示的时间?**这三个答案不同,处理方式就不同。

DATETIME 和 TIMESTAMP 不是同一种时间

MySQL 的 DATETIME 保存年月日时分秒,本身不附带时区。写入普通 2026-08-29 08:00:00,再按 DATETIME 读取,通常还是这组字段。它适合“营业时间是早上八点”这类墙上时间。TIMESTAMP 则按连接的 session 时区与 UTC 转换,适合表示时间线上的事件时刻;原始时区名称也不会保存在列里。MySQL 手册和Connector/J 的时间说明对这一区别有明确描述。

Java 侧也要配对理解:LocalDateTime 只有年月日时分秒,不单独指向全球时间线的某一刻;Instant 表示确定瞬间;Timestamp 常按确定瞬间参与 JDBC 转换。因此,同一列 DATETIME 用 getObject(..., LocalDateTime.class) 与 getTimestamp(...) 读取,可能看到不同的表面时间。

八小时差是怎样出现的

假设数据库里存着 DATETIME '2026-08-29 08:00:00',应用把这组字段按 UTC 解释为一个瞬间,而 JVM 默认时区是 Asia/Shanghai。这个瞬间在 JVM 的本地时区显示,就变成 2026-08-29 16:00:00。数据库里的字面值没有自动改成 16 点,变化发生在“把无时区字段解释为瞬间,再按本地时区显示”的过程。

Connector/J 的具体行为取决于驱动版本、读取目标类型、preserveInstants 和 connectionTimeZone。官方文档指出,8.0.23 及以后 serverTimezone 是 connectionTimeZone 的旧别名;默认 connectionTimeZone=LOCAL 会假设连接时区与 JVM 默认时区一致,并不会仅因配置了它就修改 MySQL session 时区。若要让驱动设置 session,还需要 forceConnectionTimeZoneToSession=true。参数说明

我会这样排查

先从同一条连接查询数据库状态:

SELECT @@system_time_zone, @@global.time_zone, @@session.time_zone,
       NOW(), UTC_TIMESTAMP();

再记录 JVM 的 ZoneId.systemDefault()、Connector/J 版本、JDBC URL,以及代码实际调用的是 getString、getObject(..., LocalDateTime.class) 还是 getTimestamp。最后用一行固定测试数据分别读出字符串、LocalDateTime 和 Timestamp。不要只比较日志里打印的字符串,还要看它表示的时间语义。

String raw = rs.getString("create_time");
LocalDateTime wall = rs.getObject("create_time", LocalDateTime.class);
Timestamp instantLike = rs.getTimestamp("create_time");
System.out.println(raw + " / " + wall + " / " + instantLike);

如果三者的显示不同,先按实际 Connector/J 版本与连接参数复现,再决定改字段类型、Java 类型还是连接时区;不要直接在业务代码里加减八小时。

处理策略取决于业务:

  • 如果字段表示本地墙上时间,用 DATETIME 配 LocalDateTime,并在团队内约定它属于哪个业务时区。跨地区展示时,不要临时猜测这个时区。
  • 如果字段表示事件发生的确定瞬间,优先以 Instant 一类类型建模,明确数据库 session 与 JDBC 的转换策略,并做写入后换时区读取的回归测试。
  • 避免用 CST 这样的缩写作为唯一时区标识;在 Java 配置中优先写 Asia/Shanghai 或明确偏移量。

“全系统统一东八区”可以是一个有意识的业务约定,但前提是数据库连接、JVM、数据模型和所有读写路径确实遵守它。时区问题的根源往往不是少填了一个 URL 参数,而是把墙上时间和确定瞬间混在了一起。

资料:MySQL 日期时间类型 · Connector/J 时间参数 · 时间瞬间的保存说明