MySQL 8.0 迁移到云数据库那晚,我因为 `sql_mode` 差了一个参数,整站注册逻辑崩了四小时

站长杂谈 30 浏览 0 回复 返回上级

上周把跑了三年的 MySQL 5.7 本地库往云厂商的 8.0 实例迁,数据量不大,就 60 多个表、不到 200G。想着 mysqldump 一把梭,source 进去完事,结果栽在了一个从来没正眼瞧过的配置上。

迁移过程本身挺顺,导出加了 `--single-transaction --set-gtid-purged=OFF`,导入也没报错。切完连接串,前台刷了两页都正常,心说稳了。结果凌晨两点监控报警,新用户注册全炸,报错信息糊一脸:Field 'created_at' doesn't have a default value

当场懵圈。这表结构五年没动过,created_at 一直是 timestamp DEFAULT CURRENT_TIMESTAMP,怎么突然就没了默认值?

查了半天,问题出在云数据库 8.0 的默认 sql_mode 里带了 NO_ZERO_DATESTRICT_TRANS_TABLES,而我自己 5.7 的老实例早年被人手动改过,把 NO_ZERO_IN_DATENO_ZERO_DATE 去掉了。更坑的是,迁移前我导出的 SQL 文件里,有几个历史遗留的字段定义写的是 DEFAULT '0000-00-00 00:00:00'——这在 5.7 的宽松模式下能跑,到 8.0 直接建表阶段就被静默改成了 DEFAULT NULL,或者干脆拒绝创建。

但我这表是提前建好的啊?对,问题就在这里。我为了"保险",先在 8.0 上手动执行了建表语句,那些带 0000-00-00 的字段被我自己改成了 DEFAULT CURRENT_TIMESTAMP,自以为聪明。结果漏了一个从 5.6 时代遗留下来的触发器,里面硬编码了 SET NEW.created_at = IFNULL(NEW.created_at, '0000-00-00 00:00:00')。触发器没跟着表结构一起被我看,导入数据的时候它又原样进去了。8.0 执行触发器时,STRICT_TRANS_TABLES 模式下这个非法日期直接抛错,而应用层的错误处理又把这个吞成了静默失败,用户点注册按钮没反应,后端日志里才有一行。

复盘下来,这次迁移踩了三个连环坑:

第一,sql_mode 不是"兼容就行",得逐条对。 别只看版本号,SELECT @@sql_mode; 两边各跑一遍,diff 一下。我后来写了个小脚本,迁移前自动对比源库和目标库的几十个关键变量,sql_modecharacter_set_servercollation_serverlower_case_table_names 这些全列进去。云厂商给的"兼容参数模板"往往只保大面,细节得自己抠。

第二,别信"结构没问题就只导数据"。 我那次为了快,表结构是提前手建的,触发器、事件、存储过程却靠 dump 文件后续补。结果触发器里的老语法和手动调整后的表结构对不上。现在我的流程是:结构、触发器、事件、存储过程,全部单独导出审查,用 --no-data--triggers --events --routines 分开处理,中间加一道 pt-query-digest 或者至少肉眼过一遍有没有硬编码的魔法值。

第三,回滚方案不能是"再导回去"。 我那晚花了四小时,其实前一小时就定位到问题了,但不敢直接改生产云库,怕越动越乱。回滚到 5.7 本地库?DNS 切回去要 TTL,数据已经写入 8.0 了,两边不同步。最后是靠临时把云库的 sql_mode 去掉 STRICT_TRANS_TABLES 才让业务恢复,但这本身就是技术债。现在我做迁移,必开双向同步至少跑 24 小时,用 Canal 或者云厂商的 DTS,确认延迟和一致性没问题才敢切流量,且保留回切能力。

还有个副产品:这次之后我把所有 0000-00-000000-00-00 00:00:00 从代码和数据库里扫了一遍,统一改成 1970-01-01 或者允许 NULL。MySQL 8.0 已经彻底不支持零日期了,早清早干净。

迁移这事,数据能过去只是及格线,那些藏在触发器里的老习惯、被厂商"优化"过的默认参数、你以为兼容其实偷偷变了的语义,才是真正耗时间的。你们有没有被 sql_mode 或者类似"默认配置差异"坑过的经历?

评论0
回复 · 0
还没有回复
微信客服 微信客服