如果你正在被 MySQL 时间时区修改这几个字搞得头大——查出来 time_zone 还是 SYSTEM,set global 用完之后隔一阵子又在早晨准时出问题,或者改了配置文件 default-time-zone 重启了根本没变化,那这篇文章就是给你准备的。MySQL 的时区看起来只是一个小变量,实际上牵扯到操作系统时区、全局变量、会话变量、连接层配置,还有 mysql 库里那几张不显眼的时区表,任何一个环节没对齐,“差8小时”就会像牛皮癣一样反复发作。我会把三种修改手段——动态 set global、会话级设置、配置文件 default-time-zone——从原理到操作拆开讲,包括踩坑最多的连接池缓存问题,争取让你看完就能自己去排。
1. 先别急着改,搞清楚 MySQL 的时间是怎么“歪”的
1.1 三层时区:操作系统、全局变量、会话变量
很多新手会直接搜“mysql时间时区修改”,然后抄一条SET GLOBAL time_zone = '+08:00'就跑了,结果经常发现没什么用。问题就出在 MySQL 的时区并不是一层,而是三层。
第一层是操作系统时区。mysqld 进程在启动的时候会去读系统的时区配置,如果 MySQL 参数里没显式指定,它就用一个叫SYSTEM的东西兜底,表示“我跟随操作系统”。所以你在容器里跑 MySQL,宿主机是 UTC,容器里也是 UTC,MySQL 看到的世界就全是 UTC 时间,而你业务方在本地看数据库里的NOW(),自然就差了 8 个小时。
第二层是 MySQL 全局变量层。MySQL 内部有一个全局变量time_zone,它决定所有新建连接默认拿到的会话时区。你可以通过SET GLOBAL在运行时改它,也可以通过在配置里写default-time-zone在启动时改它。注意,这一层只决定“默认值”,不直接强制改掉已经存在的连接。
第三层是会话变量层。每一个客户端连接都有自己独立的time_zone会话变量,默认继承全局值。你可以针对当前连接单独执行SET time_zone = '+08:00',只影响你自己。
这三层关系捋顺了,你就能解释很多奇怪现象:为什么别人改完立刻好了,你改完没反应;为什么服务器重启后旧问题又回来了。因为每个人改的根本不是同一层。
1.2 从一条 SQL 看时区到底作用于哪里
先别急着写配置,我建议你先做一次“时区体检”。用一个最土的办法:连上 MySQL,直接执行下面这条 SQL:
SELECT @@global.time_zone AS global_tz, @@session.time_zone AS session_tz, NOW() AS now_time, UTC_TIMESTAMP() AS utc_time, TIMEDIFF(NOW(), UTC_TIMESTAMP()) AS offset;NOW()返回的是当前会话时区下的时间,UTC_TIMESTAMP()返回的是 UTC 时间,TIMEDIFF直接算出当前的偏移量。如果offset是08:00:00,说明会话时区已经按东八区在走了;如果两个时间一模一样,说明你还在用 UTC 或者 SYSTEM。
这个体检结果很有用。以后排查问题时,第一件事不是翻配置,而是先跑一遍这条 SQL,确认你到底站在哪一层、当前偏移到底是多少。我见过太多人一边嘴上说“我改成东八区了”,一边SESSION time_zone还是SYSTEM,这不是矛盾,是他只改了全局,没看自己当前连着的老连接。
另外一个容易忽略的点是:CURRENT_TIMESTAMP、CURDATE()这类函数都跟着会话时区走,UNIX_TIMESTAMP()和FROM_UNIXTIME()在互相转换时也会套用会话时区。也就是说,你改了时区,不只是表面上多打印 8 小时,而是整个时间类型转换链条全部跟着变。
2. 临时救急:set global 改时区的完整操作
2.1 一条命令改全局,但别指望它一次生效
set global是 MySQL 运行时修改全局变量的标准手段,不需要重启服务,适合在线上临时改。比如业务方半夜打电话说时间不对,你先用一条命令把全局时区拉回来,让新连接恢复正常,这是最快速的止血方式。命令长这样:
SET GLOBAL time_zone = '+08:00';执行完别急着欢呼,立刻验证一下。注意,这里要用SHOW GLOBAL VARIABLES而不是SELECT @@session.time_zone:
SHOW GLOBAL VARIABLES LIKE 'time_zone';因为SHOW GLOBAL VARIABLES看的是全局值,而@@session.time_zone看的是当前会话值。很多人就是栽在这一步:执行完 set global,顺手看@@session.time_zone,发现还是 SYSTEM,以为自己没改成功,反复执行好几遍,其实全局早就改好了。
真正的坑在于:这个改动只对新建立的连接生效。你自己当前这个命令行连接,会话变量还是老样子,所以看起来像没生效。想要当前连接也立刻生效,可以再补一句SET time_zone = '+08:00',它只影响你当前这个会话。如果线上有连接池,里面已经缓存了一堆就连接,这些连接不会自动感知全局变化,关于这一点我后面专门讲。
2.2 权限、报错和验证流程
如果你用的是普通账号,执行 set global 大概率会遇到权限拦截,报错信息一般长这样:
ERROR 1227 (42000): Access denied; you need (at least one of) the SUPER, SYSTEM_VARIABLES_ADMIN privilege(s) for this operation在老版本里需要SUPER权限,新版本把权限拆得更细,需要SYSTEM_VARIABLES_ADMIN之类的动态权限。所以第 1 步是确认账号权限够不够,别拿只读账号去操作,白折腾。
正常的验证流程建议这么走:先确认权限,再执行 set global,再开一个新连接去查,或者在当前连接执行SET time_zone = '+08:00'后查。新连接验证才是王道,因为线上应用走的是新连接。我习惯在改完之后,从另一个终端再连一次 MySQL,执行SELECT NOW(), UTC_TIMESTAMP()看看偏移对不对。只有新连接验证通过,才敢跟业务方说“止血完成”。
2.3 为什么 set global 解决不了长期问题
set global 有一个天生的缺陷:它只管内存,不管磁盘。mysqld 一旦重启,全局变量就按启动参数或配置文件重新初始化,你 runtime 设的值全部归零,又回到 SYSTEM,差 8 小时问题准时复发。
所以我的判断标准很简单:set global 是临时救急,不是根治方案。如果只是临时顶一下,比如等业务窗口期再重启,那没问题。如果指望 set global 一劳永逸,那你迟早会在凌晨被报警电话叫醒,因为数据库重启或者主从切换之后,时区又变回去了。
根治还是要落到配置文件上,也就是标题里的default-time-zone。
3. 重启不失效:配置文件 default-time-zone 的正确姿势
3.1 配置文件里只改 [mysqld] 这一段
配置文件方案的核心是在 MySQL 服务端的配置节里写入default-time-zone。Linux 下一般是my.cnf,Windows 下是my.ini,具体路径可以通过mysqld --help --verbose | grep defaults-file之类的方式确认。我只说最关键的:一定要写在[mysqld]这个段落下面,而不是[client]、[mysql]或者文件末尾随便一个地方。
正确的写法是:
[mysqld] default-time-zone = '+08:00'如果你误写成了time_zone = '+08:00',MySQL 在启动时通常会直接忽略,或者在错误日志里留下一行看不懂的提示。这块我已经看到不止一个人栽过。写在哪一段、变量名是default-time-zone而不是time_zone,这两个细节任何一个错了,重启后都是无效变更。
修改完配置文件后,必须重启 mysqld 才能生效:
systemctl restart mysqld不同发行版的服务管理命令可能不一样,但效果都一样:让进程按新配置重新启动。重启完成之后,再跑一次第 1 节那条体检 SQL,确认@@global.time_zone变成了+08:00。
3.2 用偏移量还是用命名时区
配置default-time-zone的时候,有两种写法:一种是我上面写的偏移量写法'+08:00',另一种是命名时区写法,例如'Asia/Shanghai'。两者看起来都能让时间变成东八区,但背后的机制完全不同。
偏移量写法最省事,它不依赖 MySQL 时区表,不查任何规则,就是硬生生指定 +8 小时。中国这边没有夏令时这种概念,用偏移量完全够用,而且不会引入额外复杂度。
命名时区写法更灵活,它支持夏令时规则,比如某些美洲、欧洲时区,一年中不同季节偏移量不一样,必须靠命名时区来正确换算。但前提是 MySQL 的 mysql 库里必须已经加载了 IANA 时区数据,否则一写Asia/Shanghai,MySQL 直接报错。这个加载动作我下一节专门讲。
我个人建议,业务不涉及夏令时,直接用偏移量。别把简单问题复杂化。你要是写成命名时区,还得额外维护时区表,而且跨版本升级时表数据可能还要重新处理。
3.3 重启验证与存量连接的取舍
改完配置文件重启后,MySQL 全局变量的默认值会变成你配置的值,但这同样只影响新连接。已经存在的连接呢?重启都重启了,所有旧连接自然都断了,连接池里的连接会重连,所以重启方案反而比 set global 更彻底——不存在“旧连接还抱着老时区”的问题。
但这里有一个必须提醒的运维细节:重启 mysqld 不是无痛的,它会打断所有正在执行的业务。所以配置文件方案不能随手改随手重启,得放在维护窗口期做,或者至少确认业务低峰期能容忍一次秒级闪断。如果是云数据库,控制台里一般会有参数组,修改参数后通常会提示“需要重启实例”,效果类似。
4. 配了 default-time-zone 还是不生效?八成缺了时区表
4.1 什么时候会报 Unknown or incorrect time zone
如果你在配置里写了default-time-zone = 'Asia/Shanghai',重启后 MySQL 直接起不来,或者在错误日志里看到类似Unknown or incorrect time zone: 'Asia/Shanghai'这样的信息,那八成不是配置语法问题,而是 MySQL 的时区表里根本没有这条数据。
MySQL 内置了一个叫mysql的系统库,里面有五张表和时区相关:time_zone、time_zone_name、time_zone_transition、time_zone_transition_type、time_zone_leap_second。这些表平时是空的,除非做初始化导入。默认安装的 MySQL 只会认识一个SYSTEM,其他时区名一概不认识。
所以你配了命名时区,就好比你在手机里设置了一个从来没下载过的城市时区,系统自然不认。解决办法是把系统的 zoneinfo 数据导入 MySQL。
4.2 时区表加载操作与注意事项
在 Linux 常见发行版上,加载动作一般用一条命令完成:
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql它的原理是读取操作系统/usr/share/zoneinfo目录下的时区定义文件,转换成 SQL 插入语句,再写入 mysql 库的时区表。命令执行完以后,不需要重启 mysqld,新连接就可以直接使用命名时区了。
有几个细节要留意。第一,导入必须用有足够权限的账号执行,root 是最省事的。第二,导入过程中会产生大量 INSERT,正常会刷屏,这是正常现象。第三,如果系统 zoneinfo 目录下存在posix、right这类子目录,有些版本的mysql_tzinfo_to_sql要求特殊处理,具体看一眼命令的--help输出,别把子目录数据导重复了。
如果你用的是 Windows 环境的 MySQL,通常没有系统的 zoneinfo 文件,命名时区导入会比较折腾。这种情况下我强烈建议直接用偏移量+08:00,绕开时区表依赖,省得给自己找事。
5. 数据库时区改完了,应用还差 8 小时?连接层的坑
5.1 连接串里的 serverTimezone 才是另一只“程咬金”
很多团队踩过一个经典场景:MySQL 的时区已经通过配置文件改成+08:00了,用命令行查NOW()也完全正常,但应用打印出来的日志、插入的数据还是差 8 小时。查到最后发现,问题出在应用的数据库连接串上。
以 Java 的 JDBC 连接为例,连接串里可以这样指定时区:
jdbc:mysql://127.0.0.1:3306/dbname?serverTimezone=Asia/Shanghai如果你的应用服务器自身时区是 UTC,但连接串里又没指定 serverTimezone,驱动在建立连接时可能会用应用服务器的本地时区去解释数据库返回的时间,结果就是数据库存的是对的,驱动一层一换算,应用里看到又偏了 8 小时。
连接层时区和 MySQL 会话时区必须保持逻辑一致:要么都是+08:00,要么都是Asia/Shanghai,别一边是东八区、一边是 UTC,两层偏移叠在一起,神仙都救不回来。新版连接驱动参数更细,比如connectionTimeZone、forceConnectionTimeZoneToSession等,核心思路没变,就是让驱动、会话、应用三个时区对齐。
5.2 TIMESTAMP 与 DATETIME 的行为差异
时区配置改对了,不代表万事大吉,还得看业务表里用的是哪种时间字段。
DATETIME类型保存的是字面值。你写入2024-01-01 12:00:00,读出来就是2024-01-01 12:00:00,MySQL 不做任何时区换算,纯粹当字符串存。这种类型适合存业务发生时刻,比如下单时间,展示层的时区问题由应用自行处理。
TIMESTAMP类型则完全不同,它在存储时会把会话时区下的时间转换成 UTC 再存,读取时再转回当前会话时区。所以同一个TIMESTAMP值,你换一个会话时区去查,查出来的显示结果就不一样。这既是它的灵活性,也是它容易让人困惑的地方。
比如你的表里有TIMESTAMP字段,会话时区从SYSTEM(UTC) 改成+08:00,历史数据查询结果会整体多 8 小时,这不是数据坏了,是换算规则变了。排问题时如果发现”时间字段错乱”,十有八九就是这个机制在起作用。
另外,老版本的TIMESTAMP有 2038 年范围限制,很多业务为了省心直接用DATETIME。新版本底层时间表示已经往更宽的范围演进,但这种历史包袱在存量环境里依然存在,选字段类型时别拍脑袋。
5.3 连接池缓存会话时区的坑
还有一个特别隐蔽的坑:连接池。应用启动时,连接池会预先创建一批数据库连接,每个连接在建立时都会继承当时的全局时区。你后来 set global 改时区,或者改配置重启了但应用进程没重启,池子里的老连接依然保有旧时区。
这时候现象很诡异:数据库全局变量是对的,新建命令行连接也是对的,但应用日志还是错的。因为应用拿的是池子里的旧连接,旧连接的会话时区根本没更新。
处理办法无非三种:重启应用、让连接池重新创建连接(比如触发连接回收)、或者直接在应用层初始化时显式执行SET time_zone = '+08:00'。第三种方式最稳妥,因为它在每次连接建立时都强制执行一次会话时区,不依赖全局默认值。很多工业级的连接池初始化脚本里都会带上这条,目的就是防“全局改了对不上会话”。
6. 实战排查记录与速查表
6.1 三个典型的排查实录
我先列几个真实环境里反复遇过的场景,你看有没有你熟悉的。
第一类:改了配置不生效。有人在my.cnf里写了default-time-zone,但写到了[client]下面,重启后SHOW GLOBAL VARIABLES LIKE 'time_zone'还是SYSTEM。这类问题最快定位方法就是检查配置节,确认变量名和所在段落。
第二类:set global 后应用没变化。前面说过,连接池缓存了旧连接,全局改动对新连接生效,但应用还抱着旧连接。这类问题最快验证方法是看应用日志时间,如果命令行正确、应用不对,那就去查连接池和连接串。
第三类:数据库、连接串都对,但显示还是不对。这个往往出在应用框架本身又套了一层自己的时间格式化工具,或者前端展示层做了二次转换。排查到这一步,已经跟 MySQL 关系不大了,得沿着数据链路往下追,别死磕数据库。
6.2 五分钟定位时区问题的标准动作
我处理任何时区问题时,都不直接改配置,而是先按顺序执行下面的动作,五分钟内就能锁定问题在哪一层:
先看两层系统变量。
SELECT @@global.time_zone, @@session.time_zone;再看当前时间和 UTC 的差值。
SELECT NOW(), UTC_TIMESTAMP(), TIMEDIFF(NOW(), UTC_TIMESTAMP());然后看错误日志的时间戳配置,排除日志误导。
SHOW VARIABLES LIKE 'log_timestamps';接下去检查应用连接串里是否显式指定时区。最后看应用服务器/容器的系统时区。整个过程其实就是把三层时区挨个过一遍,哪一层跟预期不一致,问题就出在哪一层。
6.3 时区修改方案速查表
| 场景 | 推荐方案 | 生效范围 | 重启依赖 | 适合情况 |
|---|---|---|---|---|
| 临时止血,不重启 | SET GLOBAL time_zone = '+08:00' | 新连接 | 无 | 线上紧急调整 |
| 当前会话立刻改 | SET time_zone = '+08:00' | 当前连接 | 无 | 自己调试、临时查询 |
| 长期固定,业务无夏令时 | 配置文件写default-time-zone = '+08:00' | 全局,重启保留 | 需要重启 | 生产环境根治 |
| 长期固定,业务跨夏令时 | 配置文件写命名时区 + 加载时区表 | 全局,重启保留 | 需要重启 | 跨国业务 |
| 应用层兜底 | 连接建立后执行SET time_zone = '+08:00' | 当前连接 | 无 | 应用连接池环境 |
提示:任何修改完成之后,都建议再跑一次第 1 节的体检 SQL,用
NOW()和UTC_TIMESTAMP()的差值验证,别只靠猜。
我做数据库时间时区调整也已经踩过不少坑,后来养成的习惯倒是很简单:存储层统一用 UTC,展示层由应用按用户本地时区转换,数据库的default-time-zone要么不写、要写就写偏移量,绝不依赖SYSTEM。这套思路帮我避开了大部分时区问题,至少这些坑我不希望你再去趟一遍。