跳转到主要内容
技术

MySQL 的时区设置与确认方法 - 服务器 / 连接 / 列各自的行为

理解三层结构 - 服务器 / 连接 / 列

MySQL 的时区行为由 3 个层决定:服务器整体的默认值、每个客户端连接的会话设置,以及列的类型 (是 TIMESTAMP 还是 DATETIME)。许多错误源于开发者不区分这三层,只想着“修改 MySQL 的时区”。准确把握具体改变什么会让什么发生变化,是实现可维护运维的第一步。

服务器端的默认值在 my.cnf 或 /etc/mysql/conf.d/ 的配置文件中用 default-time-zone 指定。客户端可以在连接后用 SET time_zone = '+09:00' 修改,很多情况下由 JDBC 或 ORM 的驱动在连接时自动设置。列这一侧按类型决定了默认行为,TIMESTAMP 与 DATETIME 的工作方式有根本差异。

确认当前时区设置的方法

要确认当前的设置,执行 SELECT @@global.time_zone, @@session.time_zone 即可。global 是服务器整体的默认值,session 是当前连接所适用的值。如果显示 SYSTEM,表示并非使用 MySQL 自身的设置,而是遵循 OS 的时区,实际被解释为哪个时区可以用 SELECT @@system_time_zone 确认。

能否使用 Asia/Tokyo 这样的 IANA 时区名,取决于 mysql 数据库的时区表中是否已导入 tzdata。如果 SELECT COUNT(*) FROM mysql.time_zone_name 返回 0,就是尚未加载的状态,SET time_zone = 'Asia/Tokyo' 会因后文所述的 ERROR 1298 而失败。即使在这种情况下,'+09:00' 这样的偏移量指定也始终有效,因此按照先确认当前值、再确认有无 tzdata 的顺序来排查,是可靠的做法。

TIMESTAMP 与 DATETIME 的本质区别

TIMESTAMP 类型在内部把值转换为 UTC 保存,读取时再按会话的 time_zone 转回本地时间。这样设计使得客户端无论从世界上哪里连接,都能以各自的时区正确显示。其范围限制在 1970-01-01 00:00:01 UTC 到 2038-01-19 03:14:07 UTC,这源于 Unix 纪元秒的 32 位表示。这一上限在 8.0 系列中也没有改变。8.0.28 以后在 64 位环境中扩大的是 UNIX_TIMESTAMP() 和 FROM_UNIXTIME() 可接受的参数范围 (到 3001 年为止),TIMESTAMP 类型本身的极限仍是 2038 年。

DATETIME 类型没有时区的概念,会把输入的“2026-05-20 10:00:00”不做时区转换、原样保存。无论客户端的 time_zone 如何变化,显示值都不变,范围也宽至 1000-01-01 到 9999-12-31,这是它的特点。乍看很简单,但在从多个时区使用的系统中,如果不另外记录“这个值是哪里的时刻”,其含义就无法确定,因此在运维上反而是难用的类型。

TIMESTAMP 与 DATETIME 的区别 - 保存 / 读取 / 范围
对比项TIMESTAMPDATETIME
保存的内容在内部转换为 UTC 的时刻输入的“2026-05-20 10:00:00”原样保存
读取时是否依赖 time_zone依赖。按会话的 time_zone 转回本地时间不依赖。即使改变 time_zone,显示值也相同
可表示的范围1970-01-01 00:00:01 UTC 到 2038-01-19 03:14:07 UTC (源于 Unix 纪元秒的 32 位表示)1000-01-01 到 9999-12-31
DEFAULT CURRENT_TIMESTAMP可以使用。以会话的 time_zone 为基准转换为 UTC 后存储MySQL 5.6 以后可以使用。但改变服务器的 time_zone 后,保存值的含义也会悄然改变
如何确定值的含义类型本身表示绝对时刻,因此无需额外信息需要另外记录“这个值是哪里的时刻”

新项目推荐的构成是:将服务器的 time_zone 固定为 UTC 并使用 TIMESTAMP,把显示时的转换集中到应用层。

会话的 time_zone - 行为的核心

在客户端连接时像 SET time_zone = 'Asia/Tokyo' 这样指定后,此后 TIMESTAMP 的读写以及 NOW()、CURRENT_TIMESTAMP() 都会按指定的时区工作。使用 IANA 时区名时,需要事先在服务器上用 mysql_tzinfo_to_sql 导入 tzdata。Docker 的官方 MySQL 镜像往往没有安装 IANA 时区,这是 SET time_zone = 'Asia/Tokyo' 因 ERROR 1298 而失败的典型例子。

如果想避免导入 tzdata,也可以像 SET time_zone = '+09:00' 这样只指定偏移量。但这种方式无法表示夏令时,因此只对日本这样全年固定偏移量的地区有限地有效。在处理美国、澳大利亚等有夏令时的地区的系统中,应当切换为一律以 IANA 时区名运维的设计。

JDBC 驱动的陷阱 - serverTimezone 与 connectionTimeZone

在 MySQL Connector/J (JDBC 驱动) 中,通过在连接 URL 中附加 serverTimezone 或 connectionTimeZone 参数来明确指定时区。Connector/J 8.0 以后推荐使用 connectionTimeZone,可以指定 SERVER (尊重服务器端的设置) 或任意 IANA 名,默认值是 LOCAL (JVM 的默认时区)。如果不明确指定,在把 Java 端的 Instant 或 OffsetDateTime 写入 MySQL 时,会隐式地进行基于默认值 LOCAL 的时区转换。另外,即使指定了 connectionTimeZone,服务器端的会话变量 time_zone 也不会改变,因此如果想让会话也保持一致,需要同时指定 forceConnectionTimeZoneToSession=true。

常见的事故是:在开发环境中 Java 的 TimeZone 和 MySQL 的 time_zone 都是 JST,因此代码能够运行,但在生产环境的 UTC 服务器上出现 9 小时时差的偏移。要防止这种情况,就要在 JDBC 的 URL 中明确指定 connectionTimeZone,并在整个应用中贯彻使用 Instant 或 OffsetDateTime 的方针。遗留代码中的 java.util.Date 在内部带有本地时间信息,容易因 JDBC 的自动转换而产生偏移,迁移到 java.time API 才是根本的解决办法。

DEFAULT CURRENT_TIMESTAMP 的行为

在 TIMESTAMP 列上设置 DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 的模式,在 created_at / updated_at 的管理中被大量使用。这种情况下的“当前时刻”会以服务器会话的 time_zone 为基准在内部转换为 UTC,因此如果服务器的 time_zone 是 UTC,就会存入与纪元秒相对应的准确值。另一方面,如果服务器以 JST 设置运行,事后修改时区会改变过去数据的解释,因此改变运维方针时需要慎重考虑。

MySQL 5.6 以后也可以在 DATETIME 列上指定 DEFAULT CURRENT_TIMESTAMP。但 DATETIME 不保存时区,因此服务器的 time_zone 改变后,保存值的含义也会悄然改变。修改生产环境服务器 time_zone 的操作会影响过去数据的解释,是几乎不可逆的决定,团队全体的共识和充分的测试不可或缺。

最佳实践 - 以 UTC 统一的设计

新项目推荐的构成是:将服务器的 time_zone 固定为 UTC,使用 TIMESTAMP 类型,并在应用端处理 Instant 或 OffsetDateTime。显示时的时区转换集中在应用层,数据库中始终保存 UTC 的绝对时刻。即使在多区域部署时,数据的解释也不会产生摇摆,复制中也不会出现不一致。

如果现有系统以 DATETIME 类型与 JST 服务器的组合运行,立即全面迁移是困难的。作为最低限度的对策,逐步整理为:新列统一使用 TIMESTAMP,明确指定 JDBC 的 connectionTimeZone,并在应用日志中一定留下 UTC 偏移量。从长期来看,制定把 DATETIME 替换为 TIMESTAMP 的迁移计划,并连同“这个值是哪个时刻”的文档一起整理各列的含义,是迈向不产生时区错误的组织的第一步。

这篇文章对您有帮助吗?

相关文章