跳转到主要内容
技术

MySQL时区配置 - 服务器、连接与列类型的行为差异

三个层次 - 服务器、会话与列类型

MySQL的时区行为由三个层次控制:在my.cnf中设置的服务器级默认值、通过SET time_zone调整的会话级值,以及列类型本身(TIMESTAMP与DATETIME)。大多数时区bug都源于对这三个层次的混淆。知道针对特定现象应该修改哪一层,是可靠运维的基础。

服务器默认值通过my.cnf文件中的default-time-zone配置。会话值可在连接后通过SET time_zone = '+09:00'修改,大多数ORM驱动会在建立连接时自动执行此操作。而列的行为则完全取决于建表时选择的类型,运行时无法更改。

TIMESTAMP与DATETIME - 关键区别

TIMESTAMP在内部以UTC存储值,读取时转换为会话的time_zone。这意味着处于不同时区的客户端对同一底层时间点会看到一致的本地时间。有效范围从1970-01-01到2038-01-19,因为最初的实现使用32位Unix纪元秒表示。MySQL 8.0.28扩展了内部处理,但出于兼容性保留了文档中的范围限制。

DATETIME以字面的年-月-日-时-分-秒字符串形式存储值,不包含任何时区信息。无论会话设置如何,同一值读取结果相同,有效范围从1000-01-01到9999-12-31。DATETIME看起来更简单,但在多时区系统中正确使用反而更困难,因为存储值的含义依赖于数据库之外的上下文。

会话time_zone变量

连接后执行SET time_zone = 'Asia/Tokyo',后续的TIMESTAMP读取、NOW()和CURRENT_TIMESTAMP()都将使用指定的时区。要使用IANA时区名称,服务器必须通过mysql_tzinfo_to_sql加载时区数据。许多MySQL官方Docker镜像不包含此数据,因此SET time_zone = 'Asia/Tokyo'会报ERROR 1298,除非你先加载相关数据。

如果加载IANA数据不切实际,可以使用偏移量表示法如SET time_zone = '+09:00'。这适用于全年偏移量固定的时区,但无法表示夏令时。对于需要处理夏令时的区域(如美国或澳大利亚),IANA名称必不可少,前期加载tzdata的工作是值得的。

JDBC驱动陷阱 - serverTimezone与connectionTimeZone

MySQL Connector/J在JDBC URL中接受serverTimezone参数,8.0+版本中为connectionTimeZone。现代驱动版本推荐值为connectionTimeZone=SERVER(遵循服务器端设置)或显式指定IANA时区名。如果不指定,驱动会基于启发式方法猜测,Java的Instant或OffsetDateTime值在写入时可能被静默偏移。

常见的生产事故是:代码在本地正常运行(因为开发者的JDK和MySQL都是JST),但在生产环境的UTC服务器上产生9小时的偏移。修复方法是在JDBC URL中显式指定connectionTimeZone,并将所有java.util.Date的用法迁移到java.time类型(如Instant或OffsetDateTime)。Date携带隐式的本地时区语义,JDBC层会以出人意料的方式进行转换。

DEFAULT CURRENT_TIMESTAMP的语义

TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP这一模式广泛用于created_at和updated_at列。当前时间取自会话的time_zone,然后以UTC形式内部存储。如果服务器的time_zone为UTC,存储值与Unix纪元秒精确对应。如果为JST,经转换后同样成立,但后续更改服务器的time_zone不会追溯地重新解释已有行。

从MySQL 5.6开始,DATETIME列也可以使用CURRENT_TIMESTAMP作为默认值,但DATETIME不存储时区信息,因此更改服务器time_zone会改变此前写入的所有值的含义。这使得服务器time_zone在数据积累后成为几乎不可逆的决定,任何变更都应在充分了解历史数据解释方式的前提下规划。

推荐实践 - 全链路UTC

对于新项目,建议将服务器time_zone设为UTC,使用TIMESTAMP列,并在应用代码中以Instant或OffsetDateTime表示时间值。只有显示层才将时间转换为用户的本地时区。这消除了一整类bug,并使多区域部署变得简单,因为每个副本对存储值的解释完全一致。

对于运行在JST服务器上并使用DATETIME列的现有系统,无法一夜之间切换。务实的方案是:新增列使用TIMESTAMP,在JDBC URL中显式设置connectionTimeZone,日志时间戳带上明确的UTC偏移量,并记录每个现有列的预期含义。分几个季度逐列收紧schema,远比一刀切的全面迁移安全得多。

XB!LINE

这篇文章对您有帮助吗?

相关文章