——从"删库跑路"到"丝滑上线"的血泪史
前言
如果说微服务拆分是一场豪赌,那数据库迁移就是赌桌上最危险的那个筹码。
代码出问题了,回滚就行。但数据库呢?数据丢了就是丢了,回滚可能意味着用户账户余额归零、订单记录消失、聊天记录蒸发。
我职业生涯里经历过 4 次比较大规模的数据库迁移,从 MySQL 5.6 升 8.0、从单库拆分成分库分表、从自建机房迁到云上、从关系型数据库迁到 NewSQL。其中有两次堪称"惊魂夜",至今想起来手心还冒汗。
这篇文章,我把做过的方案和踩过的坑都摊开来聊。
一、5种数据库迁移策略
先说清楚一个前提:没有"最好"的迁移策略,只有"最适合你当前约束"的策略。你的停机窗口、数据量、一致性要求、团队能力,决定了该选哪条路。
策略一:停机迁移(Big Bang)
做法: 停服 → 导出数据 → 导入新库 → 校验 → 切流量 → 开服。
旧库 ──(dump)──> 新库 ──> 切流量 ──> 上线
优点:
- 方案最简单,几乎不需要额外的同步工具
- 数据一致性最容易保证(停机期间没有写入)
- 出问题了,旧库还在,回滚就是切回去
缺点:
- 需要停机,时间取决于数据量
- 数据量越大,停机时间越长,业务方越不爽
适用场景:
- 数据量小(GB 级别),导出导入能在可接受时间内完成
- 业务允许短暂停机(比如凌晨维护窗口 2 小时)
- 一次性迁移,不需要长期双写
我的判断标准: 如果导出+导入+校验能在 30 分钟内完成,停机迁移是最省心的选择。超过 30 分钟,就要认真考虑下面的方案了。
策略二:双写迁移(Dual Write)
做法: 应用同时往新旧两个库写数据,读流量逐步从旧库切到新库。
应用 ──写入──> 旧库
│
└──写入──> 新库
读流量: 旧库 100% → 90% → 50% → 10% → 0%
优点:
- 不停机,对用户无感
- 可以随时回滚(把读流量切回旧库)
- 新库可以灰度验证
缺点:
- 应用代码要改,写入逻辑变复杂
- 双写期间数据可能不一致(比如写旧库成功、写新库失败)
- 迁移周期长,代码里要维护两套写入逻辑,容易遗留"技术债"
适用场景:
- 不允许停机
- 数据量中等到大(TB 级别)
- 新旧库的数据模型差异不大(不需要复杂的转换)
关键细节: 双写不是"同时写两个库"那么简单。你需要考虑:
- 写失败了怎么办?重试?补偿?
- 两个库的写入顺序(先写哪个?如果第一个成功第二个失败,怎么处理?)
- 是否需要分布式事务(通常不建议,太重了)
策略三:CDC 增量同步(Change Data Capture)
做法: 利用数据库的 binlog / WAL / 变更日志,实时捕获增量变更,同步到新库。先做全量迁移,再做增量追平,最后切流量。
旧库 ──binlog──> CDC工具(如Debezium/Canal) ──> 新库
优点:
- 对应用代码零侵入(不需要改业务代码)
- 可以做到秒级延迟的实时同步
- 迁移期间旧库继续提供服务,不影响业务
缺点:
- 引入新的中间件,运维复杂度增加
- 需要处理 DDL 变更、大事务、循环写入等边界情况
- 全量+增量追平的时间窗口可能很长
适用场景:
- 数据量巨大(TB ~ PB 级别)
- 不允许停机
- 团队有能力运维 CDC 工具
- 迁移周期可以比较长(几天到几周)
常用工具: Debezium、Canal、Maxwell、AWS DMS、阿里云 DTS。
策略四:影子表迁移(Shadow Table)
做法: 在同一个数据库实例中,创建新结构的"影子表",通过触发器或应用层同步数据,验证通过后切换表名。
原表: users
影子表: users_new (新结构)
切换: RENAME TABLE users TO users_old, users_new TO users
优点:
- 切换瞬间完成(RENAME 是原子操作)
- 不需要额外的数据库实例
- 回滚也快(再 RENAME 回去)
缺点:
- 触发器会影响写入性能
- 不适合跨数据库引擎的迁移
- 表特别大时,RENAME 也可能有短暂锁表
适用场景:
- 表结构变更(加字段、拆表、改索引)
- 数据量中等
- 新旧表在同一个数据库实例中
策略五:绞杀者模式(Strangler Fig Pattern)
做法: 不一次性迁移,而是逐步把某些表/功能模块迁移到新库,旧库和新库长期共存,通过 API 层或代理层路由。
请求 ──> API网关 ──> 旧库 (用户模块)
──> 新库 (订单模块)
优点:
- 风险最低,每次只迁移一小部分
- 可以边迁移边验证
- 回滚粒度小(只回滚某个模块)
缺点:
- 系统长期运行在"混合态",运维心智负担大
- 跨库 JOIN 和事务处理复杂
- 迁移周期可能长达数月甚至更久
适用场景:
- 超大型系统,不可能一次性迁移
- 微服务拆分 + 数据库拆分同时进行
- 组织有能力管理长期双库共存
二、我踩过的3个坑
理论说完了。现在来说说我亲身经历的三个坑。每一个都让我至少熬了一个通宵。
坑一:binlog 追不上,迁移窗口无限延长
背景: 我们从一个单库 MySQL 迁移到一个分库分表的新集群。数据量大约 2TB,QPS 峰值 5000+。
方案: CDC 增量同步。全量导出用了 8 小时,然后开始追 binlog 增量。
问题: 全量导出的 8 小时里,线上产生了大量写入。CDC 工具追增量的速度大约是每小时处理 2 小时的 binlog。也就是说,增量追平需要 16 小时。
但我们以为几个小时就能追平,所以没准备足够的停机窗口。结果到了预定切换时间,binlog 还差 12 个小时的量没追完。
更糟的是: 随着时间推移,线上写入还在继续,binlog 越积越多。CDC 工具的消费速度开始跟不上生产速度,延迟从 2 倍变成了 3 倍、4 倍。
那一刻我意识到:我们陷入了"追赶陷阱"。
怎么解决的: 最终我们不得不临时扩容 CDC 的消费端,加了 4 个消费节点并行处理,同时跟业务方协商,在凌晨低峰期短暂限流写入。花了 6 个小时才追平。
教训:
- 全量迁移期间,估算 binlog 增量时,要按峰值 QPS 算,不要按平均值算。
- 如果全量迁移时间超过 4 小时,提前准备"追赶方案"——多消费节点、临时限流、或者考虑分表并行迁移。
- 永远假设 CDC 追增量会比你预期慢。 给它预留 2-3 倍的缓冲时间。
坑二:字符集不一致,中文数据变乱码
背景: 从自建 MySQL 5.6 迁移到云上 RDS MySQL 8.0。
问题: 迁移完成后,测试发现部分中文数据变成了乱码。排查发现:
- 旧库的字符集是
utf8(注意,MySQL 的utf8不是真正的 UTF-8,它最多支持 3 字节,存不了 emoji 和一些生僻字) - 新库的字符集是
utf8mb4 - 导出时用的工具默认按旧库字符集导出,但导入时新库按
utf8mb4解析,导致部分 4 字节字符被截断或替换
更隐蔽的是: 不是所有中文都出问题,只有那些包含 emoji 或者某些特殊字符的记录才中招。所以第一轮冒烟测试没发现,到了 UAT 阶段才被用户报出来。
怎么解决的: 重新导出,指定 --default-character-set=utf8mb4 参数,确保导出和导入的字符集一致。然后写了个脚本扫描所有文本字段,找出可能损坏的记录,从旧库重新同步。
教训:
- 迁移前,先
SHOW CREATE TABLE和SHOW VARIABLES LIKE 'character_set%'确认字符集。 - 导出和导入工具要显式指定字符集,不要依赖默认值。
- 数据校验不要只抽样,要全量比对关键字段。 至少对文本字段做长度校验和内容哈希校验。
坑三:外键约束导致切换后写入失败
背景: 把几个关联表从一个库迁移到另一个库(跨库迁移,不是同实例)。
问题: 旧库里有外键约束。迁移时,我按照表的大小顺序导出导入——先导小表,再导大表。看起来没问题。
但切换后,应用开始报外键约束错误:Cannot add or update a child row: a foreign key constraint fails。
原因: 切换时,旧库还在接受写入。我做了最后一轮增量同步,但同步的顺序是先同步子表,再同步父表。结果父表里有一条新数据还没同步过来,子表就已经引用了它的 ID。
更糟的是: 这个问题不是立刻暴露的。切换后的前 10 分钟一切正常,然后随着新写入积累,错误越来越多。
怎么解决的: 紧急回滚,把流量切回旧库。然后重新设计同步顺序——先同步父表,再同步子表。并且切换时短暂停止相关表的写入(用只读模式),确保最后一轮增量同步完成后,新旧库数据一致,再切流量。
教训:
- 有外键约束时,同步顺序必须严格遵守依赖关系。 父表先于子表。
- 切换窗口内,考虑对相关表加写锁或进入只读模式,哪怕只有 1-2 分钟。
- 切换后的前 30 分钟是最危险的。 这段时间要盯着错误日志和监控,不要以为"能访问了"就万事大吉。
三、总结:我的迁移检查清单
经历了这些之后,我现在每次做数据库迁移,都会过一遍这个清单:
迁移前
- 确认数据量和停机窗口的匹配度
- 确认字符集、排序规则、时区设置
- 确认外键、触发器、存储过程、事件调度器
- 备份!备份!备份!(并且验证备份能恢复)
- 准备回滚方案,并且演练一次
迁移中
- 全量导出导入的进度监控
- CDC 增量追赶的速度 vs 生产写入速度,实时对比
- 切换窗口的写入控制(限流/只读/短暂停写)
迁移后
- 数据校验(行数、关键字段哈希、抽样比对)
- 应用日志监控(前 30 分钟最关键)
- 性能指标对比(响应时间、慢查询)
- 旧库保留至少 7 天(不要急着删)
最后
数据库迁移这件事,说难也难,说简单也简单。难的不是技术,是敬畏心。
每一次迁移,本质上都是在跟数据的安全性博弈。你多一分准备,数据就少一分风险。
我踩过的坑,希望你能绕过去。如果绕不过去——至少,别在凌晨三点才发现。
