MySQL时区修改全攻略:从set global到default-time-zone避坑指南 1. 时区问题为什么值得专门捋一捋做后端开发的人基本都遇到过这么一幕数据库里存的好好的时间查询出来一看莫名其妙少了8个小时或者Java服务通过JDBC连上数据库之后时间戳对不上日志里的时间和业务时间差了好几个时区。排查到最后大概率会落到MySQL的时区配置上。MySQL的时间时区修改看着是个小问题但它牵涉的面非常广服务器操作系统时区、MySQL系统变量、JDBC连接参数、应用层代码、甚至容器编排配置任何一环没对齐就会出现数据没变但读出来不对的诡异现象。特别是数据库里的datetime、timestamp两类时间字段它们对时区的处理逻辑完全不同稍不注意就会踩坑。这篇文章我打算把MySQL时区这件事从原理到实操完整过一遍重点讲set global临时修改和配置文件default-time-zone持久化修改这两条路线顺手把常用的验证命令、常见坑和排查思路一并整理出来。适合刚接触MySQL的运维同事也适合那些被时区问题折磨过的后端开发同学。2. 先搞清楚MySQL的时区体系2.1 系统时区和会话时区是两个东西很多人一上来就执行set global time_zone 08:00以为完事了其实MySQL里和时区直接相关的系统变量有两大组理解清楚再动手不然很容易陷入改了没反应的尴尬。第一组是system_time_zone它表示操作系统层面的时区是在MySQL实例启动时从操作系统继承来的全局变量运行期间不能动态修改。这里的取值通常显示为CST、UTC或08:00等它影响的是MySQL内部对系统时钟的解释。第二组是time_zone它表示MySQL当前使用的时区分全局级别global和会话级别session。全局time_zone对新建立的连接生效会话time_zone只对当前连接生效。我们平时说的修改MySQL时区绝大多数指的就是修改这个time_zone变量。打个比方system_time_zone像手机出厂时的系统语言time_zone像你在手机设置里选的语言偏好。前者不到重置系统基本不动后者随时能切。2.2 timestamp和datetime对待时区的态度截然不同这是整个时区问题里最关键的底层逻辑理解了它你才能解释为什么同样的数据在两个环境里读出来不一样。timestamp类型本质上存的是UTC时间戳它内部存储的是自1970年以来的秒数不包含时区信息。当执行查询时MySQL会把内部存储的UTC时间按照当前的time_zone变量转换后返回给你。也就是说同一个timestamp值在08:00时区下读出来是14点在00:00时区下读出来就是6点——数据没有变变的只是展示层。datetime类型则完全不同它存的就是字面量你写入的2024-06-01 12:00:00读出来就是2024-06-01 12:00:00不管当前时区设置成什么它都纹丝不动。这就解释了为什么很多老项目里datetime字段看起来没受影响而timestamp字段看起来变乱了。反过来如果你做的是全球化应用需要按用户时区动态展示时间timestamp反而是更合适的选择因为它天然带时区换算能力。2.3 查看当前时区的标准姿势动手改之前先看清楚现状这几条命令建议全部执行一遍-- 查看系统时区和当前时区 SELECT global.system_time_zone; SELECT global.time_zone; SELECT session.time_zone; -- 查看时区相关全部变量 SHOW VARIABLES LIKE %time_zone%;如果session.time_zone显示SYSTEM说明会话时区跟随全局配置如果global.time_zone也显示SYSTEM说明MySQL时区直接跟随操作系统时区。这种情况下你改了操作系统的/etc/localtime重启MySQL后时区也会跟着变但运行中的实例不会自动感知。另外MySQL还可以使用命名时区比如Asia/Shanghai。但要注意使用命名时区的前提是MySQL时区表mysql.time_zone_name等已经初始化通常是通过mysql_tzinfo_to_sql工具导入了系统时区数据。很多编译安装的MySQL实例默认时区表是空的这种情况下只能用08:00这种偏移量格式用了命名时区直接报错。3. 三种修改时区的方式3.1 set global 临时修改不用重启但要小心生效范围先给结论set global time_zone 08:00;这条命令从执行成功的瞬间开始新建立的连接会使用新时区已经存在的连接不受影响仍然沿用各自建立连接时的会话时区。这背后是MySQL的连接机制在起作用每个客户端连接建立时都会从全局变量继承一份time_zone作为自己的会话值。全局变量后续怎么变都不会反向覆盖已有会话。实操命令看这里-- 修改全局时区为东八区 SET GLOBAL time_zone 08:00; -- 修改当前会话时区 SET SESSION time_zone 08:00; -- 或者不写SESSION默认作用于当前会话 SET time_zone 08:00;这里有几个细节值得注意时区偏移量要带号且小时部分最好补零写成08:00别图省事写8:00MySQL在某些版本下对格式很敏感。set global只负责全局层不会改你当前会话的时区。你如果想立刻验证效果需要重新连接一下或者顺手再执行一条set session time_zone 08:00;。MySQL的time_zone变量允许的合法取值是SYSTEM、08:00这种偏移量、或者时区表里存在的命名时区随便填个abc会直接报错。3.2 配置文件 default-time-zone一劳永逸的持久化方案set global本质上只能管到MySQL实例存活期间。实例一重启全局变量恢复成配置文件里的值或者系统默认值。所以真正要持久化必须写配置文件。Linux环境下MySQL的配置文件通常在/etc/my.cnf、/etc/mysql/my.cnf、/etc/my.cnf.d/目录下具体看你的安装方式。修改方式[mysqld] default-time-zone 08:00注意必须是[mysqld]段写在[client]或[mysql]段下不生效。default-time-zone不是变量名而是启动选项它在MySQL实例启动时把time_zone全局变量初始化为指定的值。改完之后重启服务systemctl restart mysqld # 或者 service mysql restart重启后验证SELECT global.time_zone; SELECT global.system_time_zone;如果global.time_zone返回08:00说明配置文件生效了。这里还有一个容易踩的坑有些云数据库RDS实例你根本没有权限改/etc/my.cnf控制台界面里也没有时区选项。这时候只能通过参数组Parameter Group修改default-time-zone或time_zone本质上是云平台帮你改了启动参数原理和配置文件一致但生效同样需要实例重启。在云上操作前先确认你用的是哪种购买方式别照着传统自建服务器的思路硬试。3.3 其他辅助方式启动参数、连接串参数除了上面的两条主路线实际工作中还经常用到另外两种曲线救国的手段。第一种是启动时指定--default-time-zone参数。适合不方便改配置文件、又需要临时起一个实例的场景mysqld --default-time-zone08:00 --port3306这种方式的效果和配置文件一致属于一次性启动参数进程退出就没了。第二种是在JDBC连接串上指定时区这是应用侧最常见的兜底方案。Java应用连MySQL时如果MySQL端时区不对或者不想改服务端配置可以在JDBC URL上追加参数jdbc:mysql://localhost:3306/mydb?serverTimezoneAsia/ShanghaiuseLegacyDatetimeCodefalse需要注意JDBC的serverTimezone参数在MySQL Connector/J 8.x里会优先被应用解析它相当于告诉驱动数据库在哪个时区驱动据此来做客户端的时间转换。如果这个参数和MySQL实际的时区不一致会出现时间整体偏移。这种方案的优点是应用可控、不依赖DBA配合缺点是每个应用都要配配置分散容易漏掉一两个。我见过不少生产事故就是新上线的服务忘了配serverTimezone结果在全链路排查时发现只有新服务的时间差8小时。3.4 三种方式的适用场景对比修改方式生效范围重启后是否保留适用场景set global time_zone新建立的连接不保留临时调整、线上快速止血配置文件default-time-zone所有连接重启后保留永久修改、环境标准化JDBCserverTimezone单个应用连接保留在应用侧应用级定制、多应用共存启动参数--default-time-zone实例启动级别取决于启动方式测试实战环境、容器启动脚本实际项目里我的建议是生产环境优先改配置文件临时排查问题用set global应用与数据库时区不一致时用连接串参数做兜底三层配合才最稳。4. 实操过程记录4.1 场景一线上临时把数据库时区改成UTC某个海外项目上线第一晚运维发现所有timestamp字段的时间比本地时间快了8小时因为数据库服务器操作系统用的CST中国标准时间但业务希望统一用UTC存储和展示。和业务方确认后决定先把运行中的实例态改成UTC不重启——大晚上重启数据库影响面太大。操作如下mysql -uroot -p-- 确认当前状态 SELECT global.time_zone, session.time_zone; -- 显示: SYSTEM | SYSTEM -- 修改全局时区为UTC SET GLOBAL time_zone 00:00; -- 当前会话立即验证注意当前会话要单独设置才能看到变化这里顺便验证一下 SET SESSION time_zone 00:00; SELECT NOW(), CURRENT_TIMESTAMP;此时新连接进来拿到的时区就是UTC逻辑上业务的时间展示恢复正常。但这里有个隐藏问题修改前的存量连接比如已经建立的长连接池里的连接还是带着旧时区。如果业务刚好用了连接池连接没有重建那问题不会立刻消失。排查时要留意连接池是否设置了connectionTestQuery或空闲连接回收策略否则你会在改了没效果的怪圈里转半天。果然过了几分钟业务侧反馈还有一小部分请求时间不对。我让他们把连接池最大生命周期调短或者把druid之类的连接池配置改成phyTimeoutMillis短一点让旧连接尽快被回收重建问题很快解决。这个案例里有几条经验值得记住set global对长连接连接池不友好紧急情况下必须考虑让连接尽快重建。时间类型字段读出来不对先区分是timestamp还是datetime再看时区方案合不合适别一上来就改时区。全局时区改成UTC后应用侧展示时间时还要考虑用什么时区展示否则又会出现前端展示的偏差。4.2 场景二修改配置文件并验证生效另一个客户环境是自建的MySQL 5.7实例操作系统时区和业务时区长期不一致DBA决定直接在配置文件里固定时区省得每次重启都要手动set global。首先找到配置文件位置。我用了一条命令确认当前mysqld进程读取的配置文件路径mysqld --verbose --help | grep my.cnf | head -5或更直接mysqladmin variables | grep -i config多数情况下CentOS的MySQL 5.7读取顺序是/etc/my.cnf /etc/mysql/my.cnf /usr/local/mysql/etc/my.cnf ~/.my.cnf检查/etc/my.cnf后发现里面只有一个[mysqld]段的datadir和socket配置。我在[mysqld]段下追加了一行default-time-zone 08:00保存退出后先做个语法检查再重启mysqld --validate-config systemctl restart mysqld重启后执行SELECT global.time_zone; -- 输出 08:00这说明启动参数已经生效。随后再插两条测试数据验证timestamp和datetime的行为差异CREATE TABLE time_test ( ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP, dt DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO time_test (ts, dt) VALUES (NOW(), NOW()); SELECT ts, dt FROM time_test;可以看到两个字段显示一致因为当前会话时区恰好也是08:00。接着把当前会话时区改成UTC再看SET SESSION time_zone 00:00; SELECT ts, dt FROM time_test;此时ts字段会显示成8小时前而dt字段保持不变。这个对照实验强烈建议自己动手跑一遍做一次就彻底理解两种类型的差异了。4.3 验证修改是否生效的完整命令集不管用哪种方式修改最后的验证步骤都应该形成一套命令集方便出问题时快速对照。我自己常用的是-- 1. 看系统时区和运行时区 SELECT global.system_time_zone AS system_tz, global.time_zone AS global_tz, session.time_zone AS session_tz; -- 2. 看当前时间 SELECT NOW() AS current_time; -- 3. 看UTC时间 SELECT UTC_TIMESTAMP() AS utc_time; -- 4. 计算当前时区相对于UTC的偏移 SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP()) AS offset;NOW()返回的是当前时区的时间UTC_TIMESTAMP()返回的是UTC时间。两者差值就是当前时区偏移量。比如你期望时区是东八区那么TIMEDIFF结果应该是08:00:00如果显示00:00:00说明时区还是UTC或SYSTEM系统为UTC。这套命令在排障时非常实用能快速定位是MySQL端配置的问题还是应用端转换的问题。5. 常见问题与排查技巧实录5.1 改了set global应用侧时间纹丝不动这是反馈率最高的问题。前面提到过原因长连接不会自动继承新的全局时区。Java服务用HikariCP、Druid这类连接池初始化时建立了一批连接每条连接在建立时拿到了当时的会话时区。之后你在客户端执行set global time_zone 08:00存量连接根本感知不到。排查步骤确认应用连接池是不是用了长连接。如果是先执行set session time_zone如果你能连接到这条连接上的话实际很难更实际的做法是重启应用或者等连接池把空闲连接回收后重建。确认MySQL的wait_timeout和interactive_timeout设置如果连接一直空闲不释放长连接永远不重建这个问题会持续存在。最干净的办法是改完全局时区后顺手重启应用服务或者有计划地滚动重启避免所有连接同时断开造成业务抖动。如果你用的是PXC、MGR这类集群还要注意set global time_zone只对当前节点生效其他节点的全局变量不会同步变更。这时候必须挨个节点执行或者干脆全走配置文件统一改。5.2 JDBC连接串里serverTimezone的坑这个坑我踩过好几次典型症状是数据在数据库客户端查询是正确的但Java应用查出来后时间错乱有时差8小时有时差15小时没规律。根本原因在于Connector/J驱动里有两个参数打架serverTimezone告诉驱动数据库端所在时区。useLegacyDatetimeCode在旧版驱动中控制是否使用旧的日期处理逻辑其在Connector/J 8.0中默认值为false但如果显式设置为true会触发一套老的时间处理分支和serverTimezone互动方式不同。推荐的配置是显式指定两者jdbc:mysql://localhost:3306/mydb?serverTimezoneAsia/ShanghaiuseLegacyDatetimeCodefalse如果数据库端时区是08:00而你在URL里写着serverTimezoneUTC那驱动会认为MySQL存的是UTC时间应用再用本地时区转换结果就整体偏移8小时。你要是再叠加一层容器时区比如Docker容器默认UTC就变成多层偏差复杂度拉满。排查建议先登录MySQL端执行SELECT global.time_zone确认数据库实际时区。检查应用日志里打印的JDBC URL比对serverTimezone是否一致。在应用里增加一条SQL用SELECT NOW(), UTC_TIMESTAMP()打印结果和数据库客户端直查结果比对就能快速判断是驱动层问题还是应用层问题。5.3 命名时区 vs 偏移量为什么我输入Asia/Shanghai会报错如果你执行set global time_zone Asia/Shanghai得到如下报错ERROR 1298 (HY000): Unknown or incorrect time zone: Asia/Shanghai原因是MySQL的时区表为空。这个问题在源码编译安装或者部分精简安装里特别常见MySQL默认不会自动加载操作系统的时区数据。解决办法是用系统自带的mysql_tzinfo_to_sql工具导入mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -uroot -p mysql前提是你系统里有/usr/share/zoneinfo目录通常在tzdata包里。导入完成后mysql.time_zone系列表就会填充数据之后才能使用Asia/Shanghai这类命名时区。如果不方便导入时区表就用08:00偏移量格式这个不需要任何时区表支持所有MySQL版本通用也是我日常最推荐的方式。5.4 容器环境下的时区三件套Docker部署MySQL时时区问题比裸机更隐蔽因为容器默认大多继承自基础镜像的时区设置而官方MySQL镜像默认就是UTC。你光改MySQL配置不够还得关注容器本身的系统时区因为在没有明确设置MySQL的time_zone时它默认跟随系统时区。一种做法是在启动容器时用环境变量TZ指定时区docker run -e TZAsia/Shanghai -p 3306:3306 mysql:8.0更稳妥的做法是同时挂载/etc/localtimedocker run -v /etc/localtime:/etc/localtime:ro -e TZAsia/Shanghai mysql:8.0但在Kubernetes里我建议干脆在MySQL配置层面写死default-time-zone别依赖宿主机或容器时区彻底统一apiVersion: v1 kind: ConfigMap metadata: name: mysql-config data: my.cnf: | [mysqld] default-time-zone 08:00这样无论Pod调度到哪个节点MySQL实例的时区都是一致的不会出现同集群不同时间的问题。5.5 常见错误与应对速查表现象可能原因解决办法set global time_zone执行后旧连接无变化长连接不会自动刷新会话时区重启应用或等待连接池回收重建重启后时区又变回去了只用了set global没写配置文件修改my.cnf设置default-time-zoneAsia/Shanghai报未知时区时区表未初始化执行mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -uroot -p mysqlJDBC时间差8小时serverTimezone与数据库实际时区不一致统一两端的时区配置Docker容器时间正常但宿主机时间不对容器继承了错误的TZ或/etc/localtime启动时指定TZ/挂载localtime或在MySQL配置写死集群里节点时间不一致只改了单个节点的global变量逐节点修改或统一配置default-time-zonetime_zone变量是SYSTEM且不允许改权限不足或使用了云管平台找DBA开权限或通过参数组修改6. 我踩过的一些真实教训时区问题看似简单但在实际项目里往往不是单点问题而是多个环节串在一起形成的组合拳。分享几个我印象比较深的案例希望能帮读者少走弯路。第一个案例是一次线上告警。当时一个报表系统每天凌晨跑批统计数据总是和前一天的差几个小时。查了一圈发现应用服务器是东八区数据库服务器是UTCETL脚本里直接用了NOW()写时间结果时间戳落库后展示层又按东八区读来回一折腾数据的统计边界全部错位。那个问题的彻底修复方案其实很朴素数据库时区统一改成东八区ETL脚本里所有时间字段都改用CURRENT_TIMESTAMP应用层展示逻辑保持东八区。改完后第二天跑批结果完全正确。第二个案例是连接池的场景。一个金融项目做夜间批处理DBA在下午执行了set global time_zone 08:00但应用侧时间还是老样子。排查后确认连接池里全部是长连接而且空闲连接回收时间配置成了8小时等于当天夜里所有连接还是旧时区。当时是通过重启应用解决的之后我建议他们把连接池的maxLifetime统一调整到15分钟避免类似问题再发生。第三个案例比较冷门。有个部署在Kubernetes里的服务Pod的时区总是对不上。原因竟然是基础镜像里没有tzdata包导致系统时区文件缺失TZ环境变量根本不生效。后来在Dockerfile里显式安装了tzdata问题才解决。所以容器场景下时区问题不只是MySQL侧的事镜像基础包里没有时区数据一样会出问题。还有一个小小的细节想补充一下如果你们用的是云数据库比如RDS修改时区之前一定要看控制台是否有参数组选项。有些云厂商的default-time-zone修改需要提交工单或走特定流程直接改配置文件根本改不了因为那台机器不归你管。这种情况下就要在应用连接串或启动参数上想办法。最后再分享一个个人习惯。我维护的每个MySQL实例初始化后都会强制做三件事在my.cnf里固定default-time-zone不依赖系统时区。写入监控采集项采集global.time_zone和CURRENT_TIMESTAMP一旦发现被谁改过、或者重启后漂移立刻报警。所有应用连接URL统一通过配置中心管理serverTimezone参数明确写死不允许手工修改。这几条都是踩坑踩出来的经验。时区问题不是高深的技术难点但它像温度计一样能反映出整个技术栈在基础设施规范方面做得够不够细致。只要把底层配置做扎实、日常监控做到位大多数时区故障都能在发生之前被拦截掉。