一次数据迁移的完整复盘

作者:咖啡不加糖 发布时间: 2026-08-08 阅读量:43 评论数:0

这是我做过的最紧张的一次上线。8200 万行数据,从旧的表结构迁到新的,中间字段拆分、类型变更、还多了一张关联表。整个过程持续了 26 天,切流当晚我在公司待到凌晨 4 点。

结果是成功的,但过程中犯的错误足够写一篇长的。

背景和目标

旧表有个 extra 字段,是个 JSON 文本,塞了十几个业务属性——典型的「当时图快,后来还债」。后来需要按其中三个属性筛选和排序,JSON 里的东西建不了有效索引,查询慢到 8 秒以上。

目标是把这三个属性拆成独立列,同时把一个一对多的关系从「逗号分隔的字符串」改成正经的关联表。

方案:双写 + 回填 + 对账 + 灰度

停机迁移不可能——这是核心表,停机窗口最多 5 分钟,而回填要跑十几个小时。所以只能走在线迁移:

  1. 建新表,结构按新设计来。
  2. 开双写:写操作同时写新旧两表,新表写失败只记日志。
  3. 回填存量:后台任务按主键分批搬历史数据。
  4. 对账:全量比对,修复差异。
  5. 灰度读:按用户 id 取模逐步放量。
  6. 停双写,下线旧表

流程本身没什么新意,真正的坑全在细节里。

坑一:回填把主库打挂了(第 4 天)

回填任务我一开始设的批次是 5000 行,每批之间不休息。跑了 20 分钟,主从延迟涨到 300 秒,报表系统全线超时。

我只考虑了主库的写入压力,没考虑从库的复制能力——主库多核并行写,从库串行回放,天然跟不上。

改法是批次降到 1000 行,每批检查一次从库延迟,超过 5 秒就 sleep。回填从预计的 6 小时变成 19 小时,但全程延迟没超过 8 秒。迁移这种事,慢就是快。

坑二:忘了旧库上还有定时任务(第 9 天)

这是最丢人的一个。双写是在应用层的服务里加的,覆盖了所有 HTTP 接口。但有三个定时任务直接用 SQL 操作旧表,绕过了双写。

发现得很偶然:对账时有 6 万多条数据新表没有,且都集中在每天凌晨 2 点到 3 点,查了一圈才想起那几个任务。

教训:开双写之前,先把「谁在写这张表」查清楚。我后来会在数据库层面开一段审计日志,统计所有写入来源,比翻代码可靠得多。

坑三:对账发现 1.2 万条不一致(第 15 天)

全量对账跑完,1.2 万条数据新旧不一致。逐条分析后归成三类:

  • 时区问题(约 8000 条):旧表的时间字段是 datetime,新表我建成了 timestamp。这两者在 MySQL 里对时区的处理不同,timestamp 会做时区转换,跨时区的记录就差了 8 小时。
  • 浮点精度(约 3000 条):金额字段旧表用的 double,我建新表时想「优化一下」,改成了 decimal(10,2)。结果 double 里存的 19.999999999999996 变成了 20.00
  • 并发写入的时序(约 1000 条):回填任务读到旧数据、写入新表的这个间隙里,如果有一次业务更新,就会被回填的旧值覆盖。经典的写后读竞态。

第三类是唯一真正的 bug。修法是回填写入时加条件:只有新表记录的更新时间早于旧表才覆盖。

切流当晚

灰度用了 6 天,1% 到 5% 到 20% 到 50%,每档观察 24 小时,看三个指标:错误率、P99 耗时、实时对账任务报出的差异数。

50% 那天发现了最后一个问题:某个排序接口新旧结果顺序不同。旧表按 JSON 里的字符串排序,新表按拆出的整数列排序,「10」和「9」反了。最后跟产品沟通保持新逻辑,提前公告。

100% 切流选在周二凌晨 2 点。为什么是周二?因为周一有可能积压周末的问题,周五出事没人修。

回过头看,最有价值的两件事

  1. 实时对账任务。 它持续抽样比对,而不是一天跑一次全量。灰度期间的三个问题有两个是它先发现的。
  2. 回滚开关做在配置里,不在代码里。 读哪张表由一个配置项控制,改配置 30 秒生效,不需要发版。灰度期间我用它回滚过一次,从发现问题到止血只用了 4 分钟。

还有一条不算经验的经验:数据迁移不要顺手做优化。我那个把 double 改 decimal 的决定,动机是好的,但它让对账凭空多了一类噪音,浪费了两天时间去分辨「这是 bug 还是我故意的」。

评论