数据迁移实战:千万级表不停机平滑迁移方案
千万级表的不停机迁移,标准解法不是「导出再导入」,而是「双写 + 全量搬运 + 增量追平 + 校验 + 灰度切读」五步走,缺任何一步都会在切换那一刻变成线上故障。结论:能靠在线 DDL 解决的(加字段、加索引、改字符集)绝对不要走数据搬迁;真需要换库或拆表时,请把它当成一次持续数天的线上功能改造来排期,而不是一条运维脚本。
第一步:先确认这是不是一次真迁移
结论:80% 的「千万级迁移」其实是在线 DDL 能搞定的事,先排除掉它再谈搬迁。
如果目标只是加几个字段,MySQL 8.0 的原生在线 DDL、老版本的 pt-online-schema-change 或 gh-ost 都能做到不锁表,代价只是多一份临时表和一段 binlog 量。同理,像 Clara BBS 这类自建系统升级时新增列、新增表,走后台「系统工具 → 数据库升级」执行一次增量 DDL 即可,幂等可重复执行,根本不需要搬数据。
只有三种情况才必须真迁移:换数据库引擎(MySQL→PostgreSQL)、分库分表、业务表拆分。
第二步:搭双写,让新旧两张表同时活着
结论:双写是整个方案的地基,没有双写就没有回滚,也没有「切到一半后悔」的资格。
做法是在数据访问层加一层路由:写操作同时写旧表和新表,读操作暂时只读旧表。注意事项有三条:
- 双写必须走同一事务或带补偿。新表写失败不能阻塞旧表写入,否则主链路被迁移拖垮;把失败事件投到消息队列,由消费者重试补齐。
- 双写期间新表的唯一键冲突要显式记录。老数据里常存在历史脏数据(重复手机号、空字符串唯一键),全量搬运时才会暴露。
- 上线双写当天不要同时开全量搬运,先观察 24 小时,确认双写本身没有放大主库写入压力。
第三步:全量搬运历史数据,用主键游标而不是 OFFSET
结论:全量搬运的性能瓶颈几乎总是「深分页」和「大事务」,用主键游标 + 小批次就能同时解决。
具体参数建议:
- 批大小 500~2000 行,太大容易产生长事务和主从延迟,太小则往返开销过高;
- 游标写成 `WHERE id > ? ORDER BY id LIMIT 1000`,绝对不要用 `LIMIT 1000000, 1000`;
- 从库拉数据,写压力仍落在新库,避免把主库 IO 打满;
- 每批之间 sleep 10~50ms 限速,观察主库 `Threads_running` 和从库延迟再决定是否加速。
搬运期间新产生的数据由双写负责,所以全量阶段的数据是「快照 + 持续覆盖」,允许短暂不一致。
第四步:增量追平,看延迟不看行数
结论:判断能否切换的唯一指标是增量同步延迟稳定归零并持续一段时间,而不是「看起来搬完了」。
用 Canal、Debezium 或自研 binlog 消费,从全量开始时刻的位点起追,把变更重放到新表。追平判定标准:延迟连续 10 分钟低于 1 秒,且新表的最新写入时间戳与旧表差距小于 5 秒。如果延迟一路不降,通常是新表缺少索引、存在大事务或消费端单线程,先解决再谈切换。
第五步:校验、灰度切读、保留回滚窗口
结论:切换应该是「一小时内的开关动作」,而不是「一小时内的数据动作」——数据在切之前就已经对齐了。
校验用分块 checksum:按主键区间每 10 万行对比一次行数与关键字段汇总值,百万级以上做抽样校验即可,全量逐行比对成本往往高于收益。切读按用户 ID 哈希灰度,5% → 20% → 50% → 100%,每一步观察错误率和慢查询。最关键的一条:双写链路至少保留 1~2 周,确认新表无异常后再下线,这段时间就是你的回滚预案。
最容易翻车的几个坑
结论:迁移事故大多不是架构问题,而是细节没对齐。
时间字段时区不一致、`NULL` 与空字符串语义差异、`utf8` 与 `utf8mb4` 混用导致表情符号截断、自增主键在新表重复、触发器在搬运期间重复执行——这些都要在预发环境用一份脱敏的真实数据跑一遍。
整件事的要点只有一句:迁移的风险不在「搬」,而在「切」。把双写、校验和灰度做扎实,千万级表的不停机切换就只是一次普通的配置发布。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员





