MySQL时区8小时偏移排查:SET GLOBAL与default-time-zone配置全解析
发布时间:2026/10/19 12:04:50 锦皓数字建站

前段时间我接了一个线上问题反馈很直接后台订单列表里的下单时间比实际时间慢了 8 小时而且是稳定地慢 8 小时不是偶发。我先看了业务服务器上的date系统时间正常再看数据库SELECT NOW()结果比date少了 8 小时。MySQL 时间时区配置的锅基本锁定了。这类问题在实际项目里出现频率非常高而且表现五花八门有的是查询结果整体偏移有的是新写入的时间和旧数据对不上有的是定时任务触发时刻错了 8 小时。处理方法也五花八门有人执行SET GLOBAL time_zone有人改 my.cnf 的default-time-zone还有人让应用取数时手动加偏移量。到底哪种方式对取决于对 MySQL 时区体系的理解。这篇把“set global 修时区”和“配置文件 default-time-zone 修时区”这两条路彻底讲透顺便把连接器、时区表、排查思路一起收拾干净。1. 先把 MySQL 的时间存储体系搞清楚1.1 三层时区系统时区、全局时区、会话时区MySQL 的时区不像很多人以为的只有一个全局值实际有三层系统时区、全局时区、会话时区。很多 8 小时问题就是这三层不一致造成的。系统时区是 MySQL 启动时从操作系统读出来的保存在system_time_zone变量里。它只在启动时读取运行中不会跟着操作系统自动变化。如果启动时操作系统是 UTCMySQL 的系统时区就固定成 UTC。全局时区是time_zone变量的 global 值也叫全局时区。它是新连接建立时的默认时区。修改全局时区可以用SET GLOBAL time_zone也可以启动时通过default-time-zone配置项初始化。会话时区是当前连接自己的time_zone值。一个连接建立后如果应用执行了SET time_zone那么这个连接内的NOW()、CURRENT_TIMESTAMP、TIMESTAMP 字段转换都按会话时区走。会话时区默认继承发起连接那一刻的全局时区一旦连接建立你再改全局值也影响不到它。层级变量什么时候确定修改方式系统时区system_time_zoneMySQL 进程启动时修改操作系统后重启 mysqld全局时区全局time_zone启动参数或 SET GLOBALdefault-time-zone启动配置 /SET GLOBAL time_zone会话时区会话time_zone连接建立时继承全局SET SESSION time_zone/SET time_zone三层要分开看排查才能快。很多人一上来就执行SET GLOBAL最后发现当前连接没变化又开始怀疑权限、怀疑版本其实只是把会话层和全局层搞混了。1.2 DATETIME 与 TIMESTAMP 的存储差异才是 8 小时问题的根源很多人以为数据库里存的是“时间点”其实 MySQL 里有两个完全不同的日期时间类型行为差异巨大。DATETIME 存的是字面值。写入2024-06-01 12:00:00查询出来的就是2024-06-01 12:00:00不管会话时区怎么改这个字符串都不会变。可以把它理解成一张写着具体钟点的纸条。TIMESTAMP 存的是从 UTC 纪元开始的时间戳显示时按会话时区换算。如果会话时区是08:00同一时间点显示成 20:00会话时区改成00:00它又显示成 12:00。底层值没变但肉眼看值变了。做一个最简单的验证CREATE TABLE t_time_test ( dt DATETIME, ts TIMESTAMP NULL ); SET time_zone 08:00; INSERT INTO t_time_test (dt, ts) VALUES (2024-06-01 12:00:00, 2024-06-01 12:00:00); SET time_zone 00:00; SELECT dt, ts FROM t_time_test;结果是dt仍是2024-06-01 12:00:00ts变成2024-06-01 04:00:00。这就是 TIMESTAMP 的时区敏感性。1.3 为什么 DATETIME 字段也会整体偏移 8 小时既然 DATETIME 不转换为什么业务表大量使用 DATETIME 也会出现 8 小时偏移原因往往不在 MySQL 服务端而在写入端。比如应用用 Java 的LocalDateTime.now()生成本地时间通过 JDBC 写入 DATETIME 字段。JDBC 驱动在发送参数时如果连接参数里的时区与数据库期望的时区不一致驱动可能先做一次转换把java.util.Date翻译成 SQL 字符串时带上错误偏移。服务端看到的是已经偏移过的字面值存进 DATETIME 后自然就错了。换句话说DATETIME 字段本身不会自动偏移但进入数据库之前可能已经被应用层和驱动“调”过一遍时间。另外binlog 和备份恢复场景里也存在类似问题。mysqldump 默认启用--tz-utc会在导出文件开头写SET TIME_ZONE00:00目的是让 TIMESTAMP 数据在恢复时能还原到正确的时间点如果你手动导数据时没注意这个前缀恢复端的会话时区又不同TIMESTAMP 字段就可能偏移。DATETIME 虽然不参与转换但如果你导出的是已经错的字面值恢复后还是错的。2. 临时救场用 SET GLOBAL 动态修改时区2.1 三条命令先看清现场再动手改时区前先跑一遍诊断 SQL把三层的值一次性列出来SELECT global.time_zone AS global_tz, session.time_zone AS session_tz, system_time_zone AS system_tz, NOW() AS now_time, UTC_TIMESTAMP() AS utc_time;这里的判断方法是如果now_time与utc_time相差 8 小时说明当前会话时区是08:00。如果now_time等于utc_time而你的业务期望时间是东八区说明会话时区目前是 UTC或 SYSTEM 且系统时区是 UTC。如果global_tz与session_tz不同说明这个连接被别人SET time_zone改过连接建立之后全局变更影响不到它。系统时区直接用SHOW VARIABLES LIKE system_time_zone查。先看这三行再想怎么改能避免很多瞎操作。2.2 SET GLOBAL 与 SET SESSION 的具体写法线上救急时最常看到的是这条SET GLOBAL time_zone 08:00;GLOBAL 表示修改全局时区影响之后建立的新连接权限要求是SUPER或SYSTEM_VARIABLES_ADMIN。不是普通账号都能执行的改之前先确认账号权限。想只影响当前连接用SET SESSION time_zone 08:00;也可以简写成SET time_zone 08:00;在 MySQL 里不指定 SESSION/GLOBAL 时默认是会话级别所以这条只对当前连接生效。可用的时区值有三类一是固定偏移量如08:00、-05:00不需要加载时区表就能用二是SYSTEM意思是跟随系统时区三是命名时区前提是 MySQL 的mysql.time_zone_name表里有数据。命名时区的问题后面会讲。对于大多数只需要对齐东八区的场景直接写08:00最稳既不依赖系统时区也不依赖时区表。2.3 动态修改的边界为什么执行完 SET GLOBAL 后现有客户端还是老时间经常有人执行完SET GLOBAL time_zone08:00后马上在旧终端里查SELECT NOW()发现时间还是不对就以为命令没生效。其实SET GLOBAL只影响新连接已建立的连接在建立时就确定了自己的会话时区不会感知全局变化。如果你手里的终端就是这个连接需要再执行一次SET time_zone 08:00;把当前会话改过来。如果你的应用是长连接加连接池那么即使执行了SET GLOBAL连接池里已建立的连接仍然保持旧时区必须重启应用或者主动销毁连接池里的连接再重新建立。这也是我认为 set global 适合临时验证、不适合作为长期方案的原因。它改起来方便但作用边界很模糊线上用很容易漏掉连接池里的存量连接。2.4 验证修改是否到位修改完不要只看一条 SQL。因为SELECT NOW()受会话时区影响验证需要看全局和会话两个值SELECT global.time_zone, session.time_zone, NOW(), UTC_TIMESTAMP();如果全局已经是08:00但会话还是00:00那问题就在会话层。如果全局和会话都是08:00且NOW()与UTC_TIMESTAMP()相差 8 小时说明 MySQL 服务端已经对齐。之后再用一个全新连接测试确认应用侧。3. 永久固化default-time-zone 配置项的正确落法3.1 先找到 my.cnf再谈配置set global 再灵活重启之后就丢了。要永久生效必须把参数写进配置文件。MySQL 配置文件查找顺序一般是/etc/my.cnf、/etc/mysql/my.cnf、/usr/etc/my.cnf、~/.my.cnf各个发行版和安装方式不一样。可以用mysqld --verbose --help | grep -A 1 Default options查看当前实例实际读取了哪些配置文件。注意这里用的是mysqld而不是mysql执行前确认mysqld在当前 PATH 中。配置必须放在[mysqld]段不是[client]也不是[mysql]。放错段位是最常见的“配置无效”原因。段位就像 ini 文件的小节[mysqld]才是服务端启动参数读取的位置。3.2 default-time-zone 的取值写法在[mysqld]下加一行[mysqld] default-time-zone 08:00我建议写偏移量形式比如08:00、00:00。这种写法不需要时区表任何 MySQL 版本都能识别。命名时区写法也可以但要求时区表已经导入否则启动阶段可能报错反而增加救火成本。注意 ini 文件的值不需要加引号。虽然有些博客会写成default-time-zone08:00并且也能生效但严格来说 MySQL 读取时会把引号当作值的一部分只在某些特定解析路径下行为不一致容易造成混乱。直接裸写最稳。default-time-zone这个配置项不是动态变量。它只决定 MySQL 启动时全局time_zone的初始值修改后不会自动热更新必须重启 mysqld 进程。3.3 修改配置后重启别以为重启只是 systemctl restart重启方式取决于你的部署形态。如果用的是 systemd 管理的 MySQL通常直接systemctl restart mysqld如果用的是 mysqld_safe 托管需要先关掉 mysqld_safe 再启动否则旧 wrapper 可能又把老进程拉起来配置看起来改了但实际加载的还是旧参数。可以这样判断先ps -ef | grep mysqld看父进程是谁。父进程是 mysqld_safe就 kill 掉 mysqld_safe 和 mysqld再重新启动父进程是 systemd直接 systemctl restart。重启后验证SELECT global.time_zone;看到08:00就说明配置被正确加载。3.4 如果没配 default-time-zoneSYSTEM 到底跟随谁MySQL 默认的time_zone是SYSTEM表示“跟随系统时区”。但它不是运行时动态跟随而是在启动时把操作系统的时区读进来之后固定不变。有两个容易踩的点第一启动 MySQL 之前操作系统时区已经是 UTC那么即便后来你把/etc/localtime改成东八区MySQL 的 SYSTEM 仍然等于 UTC。很多人改完系统时区发现 MySQL 还是差 8 小时就是这个原因。第二如果同一台机器上部署多个实例或者操作系统时区批量初始化错误SYSTEM 会引入很多隐性依赖。我建议生产环境不要依赖 SYSTEM直接显式写偏移量让数据库行为和操作系统解耦。4. 连接器与中间件另一处时区战场4.1 JDBC 的 serverTimezone 参数为什么一定要写服务端改完应用侧如果不配合时间照样不对。最常见的就是 Java 生态的 JDBC。JDBC 驱动在建立连接后要搞清楚“服务器现在是什么时区”才能正确地把数据库返回的 TIMESTAMP 字段转换成java.util.Date或LocalDateTime。如果驱动猜错了服务器时区它会把已经按正确时区显示的时间再转一次造成固定偏移。MySQL Connector/J 早期版本在连接参数里有一个serverTimezone后来 8.0 引入了connectionTimeZone但核心问题没变告诉驱动服务端的真实时区。如果应用统一使用 UTCURL 直接写jdbc:mysql://localhost:3306/test?serverTimezoneUTC如果服务端固定是东八区可以写jdbc:mysql://localhost:3306/test?serverTimezoneGMT%2B8注意这里 URL 中的加号必须转义成%2B否则它会被解析成空格驱动根本识别不了。这是很多人写了serverTimezoneGMT8却依然报错的原因。4.2 Connector/J 5.1 与 8.0 的差异Connector/J 5.1 时代serverTimezone不写也能连但驱动会使用 JVM 默认时区去猜服务器时区。如果你的应用服务器时区和 MySQL 时区一致问题不大不一致就会出现 8 小时之类的问题。很多老项目的坑就是“本地没问题测试环境差 8 小时”因为本地和测试机的系统时区被配成了不同的值。Connector/J 8.0 之后行为更严格8.0.23 版本对参数做了更清晰的划分connectionTimeZone用于指定连接时区forceConnectionTimeZoneToSession决定是否把会话time_zone强置成connectionTimeZone。升级 JDBC 驱动的项目要重新检查这些参数不能只把 5.1 的 URL 原样搬上去。实际经验是与其在驱动参数里纠结不如先统一约定——应用层、MySQL 全局时区、JDBC 连接参数三者都用同一个偏移量然后参数里新项目优先写connectionTimeZone老项目至少把serverTimezone写清楚。4.3 连接池初始化时区另一个安全阀如果服务端已经配好default-time-zone连接池一般不需要额外处理。但如果你想给某个应用单独设置时区不想影响数据库全局配置可以使用连接池的初始化 SQL 功能。HikariCP 可以配置spring: datasource: hikari: connection-init-sql: SET time_zone 08:00Druid 可以配置spring: datasource: druid: connection-init-sqls: - SET time_zone 08:00意思是每个新连接建立后先执行这条 SQL 再交给应用相当于把“会话时区”在连接源头固定死。这个方式很适合多租户系统里数据库统一配置但不希望所有连接都用同一个 session 时区的场景。Python 生态的 PyMySQL 也有类似能力connector 的init_command参数可以传初始化 SQL。Go 的database/sql通常靠 DSN 参数和连接字符串来处理具体取决于驱动实现但思路一致连接层把时区固定住不要依赖隐式继承。5. 存量数据与跨区换算别一改了之5.1 先判断存量数据要不要“换算”很多人改完时区发现旧数据看起来“错”了。其实要分清数据本身错没错取决于字段类型和业务语义。如果字段是 TIMESTAMP底层存的是 UTC 时间点只是显示时按会话时区变化。MySQL 全局时区从00:00改成08:00后同一时间点显示会多 8 小时但时间点本身没变只是“展示方式”变了。如果业务期望的时间点就是当前值对应的时间点那完全不需要改存量数据。如果字段是 DATETIME底层存的就是字面值。比如之前 MySQL 和业务都按 UTC 跑表里存了2024-06-01 04:00:00现在业务切到东八区期望看到2024-06-01 12:00:00那 DATETIME 字段必须做一次 UPDATE 加 8 小时。不做的话展示端怎么转换都不对。5.2 时区表导入与 CONVERT_TZ 的实用姿势有些时候不需要改数据只需要在查询时按目标时区显示。MySQL 提供CONVERT_TZ()函数SELECT CONVERT_TZ(2024-06-01 12:00:00, 08:00, 00:00);第一个参数是时间第二个是源时区第三个是目标时区。用偏移量形式不需要时区表即可工作非常方便。如果想用命名时区做带夏令时的换算需要先把操作系统的时区数据导入 MySQL。Linux 上一般这样执行mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql这条命令把/usr/share/zoneinfo下的时区定义灌入 mysql 库的时区表。导入后连接可以用命名时区名。但要提醒导入是一次性动作已经建立的连接不会自动感知需要重连。导入失败时CONVERT_TZ对命名时区返回 NULL可以先SELECT COUNT(*) FROM mysql.time_zone_name确认表中有没有数据。如果重复导入出现主键冲突需要考虑先清空旧时区表再重新导入。5.3 存储规范建议到底存 UTC 还是存本地时间我的建议是分场景不要迷信“必须存 UTC”。跨地域业务比如面向多个时区的用户最好在数据库层统一存 UTC展示层按用户时区换算。单地域业务固定在东八区或者固定在某一个偏移量数据库层直接存本地时间更直观排查问题时不用每次都做换算。日志类数据建议存 UTC因为日志要跨服务器聚合服务器时区不一致时 UTC 最安全。订单、账户里那些“业务单据时间”如果要做对账推荐用 TIMESTAMP 类型因为它本身带 UTC 换算能力纯日期字段用 DATE需要保留原始字符串的用 DATETIME。另外记住 TIMESTAMP 的范围是1970-01-01到2038-01-19存远期时间会溢出这种字段要用 DATETIME。6. 典型故障排查与避坑清单6.1 改了 default-time-zone 却感觉没生效的排查步骤先确认配置文件加到了[mysqld]段不是[mysql]段。确认重启动作真正重启了 mysqld 进程而不是只重启了 wrapper。重启后执行SELECT global.time_zone如果还是SYSTEM或旧值说明配置没被读到。如果全局值正确但某个应用还不对去查连接池里的连接是否还是旧连接。把应用重启一下强制重建连接池再验证。最后确认 JDBC 参数里有没有写死时区比如serverTimezone或connectionTimeZone与现在数据库值不一致。这五步走完绝大多数问题都能定位到具体层。6.2 部分程序时间不对、另一部分正确的排查思路这种“部分对部分错”通常意味着不是数据库全局配置问题而是各应用连接参数不一致。先确认几件事应用服务器系统时区是什么JDBC URL 里写了什么连接池有没有初始化 SQL三个答案对齐时间就对齐。如果只有 cron 任务时间不对多半是操作系统时区问题和 MySQL 没关系先查date %z。有一点要特别提醒连接池如果没有设置初始化 SQL连接在池里的存活时间可能很长数据库全局时区改了以后池里的连接还保留旧会话时区。这时候不需要去数据库里反复执行 set global直接重启应用或清空连接池效果更直接。6.3 故障速查表症状常见原因优先处理方式全库查询结果整体比业务时间晚 8 小时系统或全局时区为00:00业务期望08:00确认系统时区或default-time-zone08:00重启验证执行 SET GLOBAL 后现有客户端不变会话时区在连接建立时已固定重连连接或执行SET time_zone08:00只有 Java 应用时间不对命令行对JDBCserverTimezone/connectionTimeZone不一致显式设置连接参数应用重启连接池复用时时间不对重启应用后恢复连接池里是旧时区的长连接配connection-init-sql或销毁连接池备份恢复后 TIMESTAMP 差 8 小时导出/导入会话时区不一致使用 mysqldump 默认--tz-utc恢复时保持同一时区定时任务触发时间偏移操作系统时区被改MySQL SYSTEM 保持旧值修改 OS 时区后重启 MySQL或改用显式偏移量6.4 完整验证脚本模板最后给一个可以直接拿去用的验证 SQL 模板SELECT global.time_zone AS global_time_zone, session.time_zone AS session_time_zone, system_time_zone AS system_time_zone, NOW() AS mysql_now, UTC_TIMESTAMP() AS utc_now, TIMEDIFF(NOW(), UTC_TIMESTAMP()) AS now_utc_diff;再看结果now_utc_diff为08:00:00说明当前会话时区是东八区。global_time_zone为SYSTEM而system_time_zone为UTC说明系统时区没弄对。global_time_zone为08:00但session_time_zone为00:00说明连接里有单独SET time_zone。把这几行 SQL 存在自己的运维工具箱里每次排查时区问题先跑一遍基本不会误判。最后聊一点个人习惯。我现在接手一个项目第一件事就是看三样东西操作系统时区、MySQL 的全局time_zone、应用连接串里的时区参数。这三者如果没有明确约定我就直接按“全部固定为同一偏移量”去纠偏而不是临时执行 set global 碰运气。改配置前先确认连接池存量连接会销毁重建改完一定用新连接验证。时区问题不算难但它横跨操作系统、数据库、驱动和应用四层任何一个环节靠“隐式默认值”迟早会在某个凌晨给你补上一课。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。