一、分析
目的:在服务不停机、流量不断、数据持续变化的情况下,怎么保证 A → B 迁移过程最终正确、可回滚、对业务影响可控。
手段:双写 / 增量同步 / 灰度读切换 / 最终下线 A
一个高并发且不能下线的服务,从库 A 迁移到库 B,通常要同时满足这几个目标:
1.业务不中断
2.数据不丢
3.数据尽量一致
4.能灰度
5.能回滚
6.可观测
所以本质上是一个:
存量数据迁移 + 增量数据追平 + 流量切换 + 验证回滚
的系统工程。
二、方案
1、先搭好库 B
2、把 A 的历史全量数据导入 B
3、迁移期间把新写入同时同步到 B
4、校验 A/B 数据一致性
5、先灰度读 B
6、稳定后切写到 B
7、观察一段时间后下线 A
这就是经典的:
全量迁移 + 增量同步 + 双写 + 灰度切流 + 回滚保护
对于一个高并发且不能下线的服务,从库 A 迁移到库 B,我一般会采用"全量迁移 + 增量同步 + 灰度切流 + 回滚保护"的方案。
第一,先完成 B 库的表结构、索引、压测和监控准备。
第二,对 A 做历史全量迁移,按主键范围或时间分片批量导入,过程要限流、可重试、幂等。
第三,在迁移期间处理增量数据,可以用应用层双写,或者更推荐用 CDC 订阅 A 的变更日志同步到 B。
第四,建立数据校验机制,包括全量行数、checksum、关键字段抽样、线上 shadow read 比对。
第五,当全量完成且增量追平后,开始灰度把读流量切到 B,按用户或租户逐步扩大比例。
第六,读稳定后再灰度切写到 B,整个过程通过配置开关控制,确保随时可以回滚。
第七,观察一段时间,确认性能、数据一致性、补偿队列都正常后,再正式下线 A。
其中最关键的是解决双写不一致、消息重复、顺序错乱、删除同步、缓存一致性和回滚能力这些问题。
text
┌──────────────────────┐
│ 配置中心/开关 │
│ read_route write_route│
└──────────┬───────────┘
│
│动态控制读写切换
▼
┌──────────┐ ┌───────────────────┐
│ Client │ ───────────────▶ │ 业务服务 │
└──────────┘ │ Service / API │
└───────┬─────┬──────┘
│ │
读请求路由 │ │ 写请求主链路
│ │
┌─────────────────┘ └─────────────────┐
▼ ▼
┌──────────────┐ ┌────────────────┐
│ 读路由层 │ │ 写入库 A │
│ Read Router │ │ Source DB A │
└──────┬───────┘ └────────┬───────┘
│ │
┌───────────┴────────────┐ │
│ │ │
▼ ▼ │
┌──────────────┐ ┌──────────────┐ │
│ 读库 A │ │ 读库 B │ │
│ Read from A │ │ Read from B │ │
└──────────────┘ └──────────────┘ │
│
│ binlog / CDC
▼
┌────────────────────────┐
│ 增量同步链路 │
│ CDC / MQ / Sync Worker │
└──────────┬─────────────┘
│
▼
┌────────────────┐
│ 目标库 B │
│ Target DB B │
└────────┬───────┘
│
│
全量迁移链路 │
┌───────────────────────────────────────────────┘
▼
┌───────────────────────┐
│ 全量迁移任务 │
│ Batch/Chunk/Checkpoint│
│ 可限流/可重试/幂等 │
└───────────────────────┘
┌─────────────────────────────────────────┐
│ 校验与观测系统 │
│ 行数校验 / checksum / 抽样比对 / 延迟监控 │
│ 错误告警 / 补偿队列 / 回滚开关 │
└─────────────────────────────────────────┘
三、细节
0、问题
数据模型是否兼容
B 是不是和 A 同构
主键是否一致
索引是否一致
字段类型是否一致
唯一约束、事务语义是否一致
一致性要求多高
能否接受短时间最终一致
是否要求强一致
是否涉及余额、库存、订单状态这类强一致业务
访问模式
读多写少,还是写多读少
是否有范围查询、复杂查询、聚合查询
是否依赖 A 的某些特性,B 没有
迁移规模
数据量多大
峰值 QPS 多大
每秒写入量多大
能否分库分表、分租户、分用户批次迁移
这一步很关键,因为:
如果是普通资料类数据,双写 + 最终一致通常够用
如果是资金、库存、计费类数据,方案要更保守,甚至要引入业务冻结点、幂等日志、事务消息
1、先建库 B
这里不是简单建表,而是要保证 B 已经具备线上承载能力:
表结构建好
索引建好
容量评估完成
压测完成
监控告警就位
权限、备份、容灾配置好
另外最好提前做两件事:
影子压测
用真实流量模型测试 B 的读写能力。
预热
热点数据、连接池、缓存、索引页尽量预热,避免切过去后抖动。
2、先做历史全量迁移
把 A 中已有的存量数据导入 B。
常见做法:
按主键范围分片扫
按时间分段扫
按租户 / 业务分区扫
批量导入,限速执行
这里注意几个点:
不要把 A 打挂
全量扫描最容易把线上库拖慢,所以要:
走从库或备库
控制 batch size
限流
在低峰期执行
迁移过程要可重试
任何批次失败都能重跑,最好是:
按 chunk 记录进度
每批有 checkpoint
支持断点续传
写入 B 要幂等
比如用:
insert on conflict update
replace into
upsert
这样重复导入不会出脏数据。
3、处理增量:让 A 上的新变化持续进入 B
光做全量不够,因为迁移过程中业务还在写 A。
所以必须有增量同步机制,常见两类:
方案 A:应用层双写
业务代码写入时,同时写 A 和 B。
例如:
先写 A,再写 B
或先写 B,再写 A
或主写一边,另一边异步补偿
优点
简单直接
易于控制业务逻辑
缺点
侵入业务代码
双写失败处理复杂
高并发下容易出现 A 成功、B 失败的分叉
方案 B:基于日志的 CDC(Change Data Capture)
从 A 的 binlog / redo / oplog / WAL 中订阅变更,再同步到 B。
优点
对业务侵入小
链路清晰
更适合大规模迁移
缺点
要处理日志消费延迟
需要解决顺序、幂等、重复消费问题
实际工程里常见建议
如果业务允许轻度改造:
全量 + CDC 增量同步 + 读灰度切换,通常更优雅。
如果必须快速落地,且业务路径可控:
全量 + 应用层双写 也很常见。
4、双写时最关键的问题:怎么避免不一致
做幂等
每次写操作有唯一 request_id / event_id,B 重试写入不重复生效。
做重试
B 写失败后进入重试队列。
做补偿
把双写失败的请求落到消息队列 / 补偿表 / outbox 表,后台异步修复。
做顺序控制
同一主键的数据变更要尽量保证顺序,比如:
同 key 路由到同一 partition
单 key 串行消费
做版本号 / 时间戳控制
避免旧数据覆盖新数据,例如:
update where version = old_version
或只接受更大 version 的更新
写路径设计成这样
业务请求先按主链路写 A
同时记录一条变更事件
同步器消费变更事件写 B
失败则重试和补偿
用版本号保证最终一致
这样比"请求线程里直接双写两份库"更稳。
因为高并发下,主请求链路最怕被第二个库拖垮。
5、什么时候切读,怎么切
只有当这两件事成立,才能开始把读流量切到 B:
历史全量已经完成
增量延迟已经追平到足够低
比如 CDC 延迟从分钟级降到秒级甚至毫秒级。
读切换最好灰度进行
不要一次性全切,建议按下面方式:
灰度维度
按用户 ID 百分比
按租户
按机房
按某个业务线
按只读接口优先
读灰度步骤
先 shadow read(影子读)
正常返回 A 的结果
同时后台读 B,比对结果,不影响用户
小流量读 B
1% → 5% → 20% → 50% → 100%
发现异常立即切回 A
为什么先影子读
因为很多问题不是"写错了",而是:
索引没建对
查询性能差
排序不一致
默认值不一致
字段精度不同
影子读能提前发现这些问题。
6、什么时候切写
一般顺序是:
阶段 1:A 为主,B 为从
写 A
同步到 B
读 A 或灰度读 B
阶段 2:读逐步切到 B
写还是 A
大部分读去 B
阶段 3:写切到 B
新写入直接进 B
A 变成回退兜底或只读
阶段 4:观察期
监控稳定
无大量补偿任务
数据校验通过
阶段 5:下线 A
7、写切换是最危险的一步
写切换时建议做成可配置、可快速回滚。
例如:
用配置中心控制写路由
按服务实例灰度
能一键切回 A
写切换前必须满足
B 已经稳定承压
数据校验通过
增量同步延迟接近 0
回滚脚本准备好了
监控和告警齐全
8、数据校验怎么做
不能"感觉没问题",要有系统化校验。
1)全量校验
总行数
按分片/分桶统计行数
主键集合抽样
checksum / hash 校验
2)增量校验
最近 N 分钟写入的数据是否一致
更新后的关键字段是否一致
删除是否同步
3)业务校验
用户能否查到自己的数据
订单状态是否一致
列表结果是否一致
聚合结果是否一致
9、删除操作尤其要小心
迁移里删除最容易出问题。
因为删除不是简单"没了",要考虑:
物理删除还是逻辑删除
删除事件是否能同步
B 是否会漏删
旧的延迟消息会不会把删掉的数据又写回来
所以一般建议:
用 tombstone / delete event
明确把删除作为一种事件同步。
用版本机制防止"复活"
例如删除版本比旧更新版本更大,旧写入不能覆盖删除状态。
10、高并发下几个容易被追问的坑
你可以顺手补这几个点,会显得很工程化。
1)热点 key
某些大用户/大商户/热门对象更新很频繁,容易导致:
双写顺序乱
同步延迟
B 上锁竞争
解决:
同 key 串行
分区路由
限流和热点隔离
2)顺序问题
先更新后删除、先删除后更新如果乱序,最终状态会错。
解决:
event version
单 key 顺序消费
last-write-wins 要慎用
3)幂等问题
补偿、重试、MQ 重投都会导致重复写。
解决:
幂等键
去重表
唯一约束
upsert
4)性能回退
B 库虽然功能对,但性能可能差。
解决:
压测
慢 SQL 预检查
影子读
热点查询 profiling
5)缓存一致性
如果上层有 Redis / 本地缓存,切库时不仅是数据库问题。
解决:
明确 cache miss 后查哪边
切换期间缓存失效策略统一
避免 A/B 混用造成脏缓存