数据迁移后,用双写加校验这套组合拳,能让新旧库记录在最终时间点上严格一致,而不是靠运气。 数据迁移不是把表导过去就结束,新旧库会同时跑业务,写入路径一旦分叉,记录就会慢慢对不上,双写负责“两边都写”,校验负责“对不上就纠偏”,两者一起才能把最终一致性变成可验证的结果。
数据迁移双写校验怎么做:先搞清楚三个关键点
很多团队在迁移后会碰到新旧库数据不一致的情况,原因往往比想象的简单:迁移脚本漏了分区表、增量同步断了没感知、字段映射写错一列,事后排查要花掉大量精力,不如一开始就把双写校验的机制定下来,要做对这件事,先理解三个关键点。
双写到底在写什么
双写不是把同一个SQL在新旧库各执行一遍那么简单,业务对象需要分别在两个存储引擎里完成持久化,而且两边的写入顺序、事务边界、失败处理都要有明确约定。
实际操作中,最常见的实现方式是应用层双写:业务代码在写完旧库后,再组装一次数据写入新库,这种方式改动最直接,也容易控制,如果不想动业务代码,可以走binlog监听,让中间件把旧库的变更同步到新库,但这属于旁路逻辑,对延迟和消息可靠性要求更高。
校验要对比哪些维度
校验的核心不是看两张表的总行数,而是看每一条业务记录是否真的能对上,建议至少做三层对比:
- 数量对比:统计表行数,看巨大缺口。
- 关键字段对比:选择业务主键和核心业务字段,逐条比对值是否相等。
- 哈希对比:把一条记录的所有字段拼接后生成MD5或CRC32,两库哈希值不一致就意味着有差异。
对于订单、用户这类敏感数据,哈希对比是更可靠的手段,字段拼接时要保证顺序固定,否则两库计算出不同结果,会误报差异。
怎样才算“最终一致”
最终一致不是强一致,双写过程中,新库可能比旧库慢几百毫秒,这是允许的,判断最终一致的标准是:在一个完整的校验周期后,两库差异记录数为零,并且这个状态能持续维持,过一会儿再看,两边一模一样”。

新旧数据库记录不一致怎么办:双写加校验的落地步骤
光有理论不够,迁移落地时踩过的坑才值钱,下面以一次订单库迁移为例,给出可执行的步骤。
第一步:给迁移切一道明确的“分水岭”
迁移前先记录一个水位线,比如旧库全量导出时最大的订单ID或更新时间,这个值决定了后续增量同步从哪里开始,如果水位线记录不准,增量数据和全量数据之间会出现空档,新库会丢失一部分写入。
具体操作是把水位线写在配置中心或一张独立的元数据表里,不要只记在某个人的聊天记录中。
第二步:开启业务层双写
业务代码里增加一个双写开关,旧库写入成功后,把同一份数据写入新库,写入失败时不要影响主流程,先记录日志,让后续校验去补,很多团队在双写开启时犯一个错误:把新库写入放在事务里,结果新库抖动连累旧库,正确做法是让新库写入变成异步任务,或者用事务后置事件驱动。
下面是一段极简的Java伪代码:
public void createOrder(Order order) {
oldDb.save(order);
asyncExecutor.submit(() -> {
try {
newDb.save(order);
} catch (Exception e) {
log.error("write to new db failed", e);
}
});
}
第三步:跑校验任务,把差异捞出来
校验任务要分片执行,别一次拉全表,按订单ID范围分片,每个线程处理一小段,避免把数据库IO打满。
脚本里只需要两个查询,分别从新旧库取出同一段ID范围的数据,拼接关键字段后计算哈希值,再放进内存集合比对。
SELECT id, md5(concat(order_no, user_id, status, amount))
FROM old_order
WHERE id BETWEEN ? AND ?;
SELECT id, md5(concat(order_no, user_id, status, amount))
FROM new_order
WHERE id BETWEEN ? AND ?;
两个结果集对比后,找出只存在于旧库、只存在于新库、哈希值不一致三类记录,写入差异表。
第四步:补偿“缺口”并循环验证
补偿的规则要提前定好,不能临时拍脑袋,默认以旧库为准,因为旧库仍然是业务的主库,对于新库缺失的记录,用旧库数据重新插入;对于哈希值不一致的记录,先查变更日志,确认哪边的数据更新鲜。

修复完一批,再跑同一分片的校验,直到差异为0,然后继续下一个分片,所有分片清零后,再整体跑一轮全量校验,确认整个表没有漏网之鱼。
双写方案对比:选同步还是异步,决定你睡不睡得着
双写方案不是只有一种,不同选择对应不同的风险,很多人会在百度上搜“双写方案对比”,其实核心就两条路:同步和异步。
| 维度 | 同步双写 | 异步双写 |
|---|---|---|
| 一致性 | 两边几乎同时可见 | 最终一致 |
| 性能影响 | 高,新库慢会拖累旧库 | 低,业务无感知 |
| 故障风险 | 新库故障可能连累主流程 | 消息丢失或积压 |
| 对校验的依赖 | 低 | 高 |
同步双写:稳但慢
同步双写会在同一个事务里把数据同时写入新旧库,或者用两阶段提交来保证原子性,好处是一致性强,坏处是性能明显下降,而且新库一个短暂超时会让整个业务跟着颤抖,行业共识认为,同步双写更适合对一致性要求极高、写并发较低的场景,比如配置中心或账户核心表。
异步双写:快但乱
异步双写通过消息队列或线程池把新库写入放到后台,业务主流程不感知,性能很好,但引入了消息丢失、重复消费、顺序错乱等问题,这些问题不会立刻暴露,只有靠定时的校验任务才能发现和纠正,所以异步双写必须和校验配合,否则“最终一致”会退化成“永远不一致”。
全量校验与增量校验怎么搭配
迁移初期跑全量校验,把历史欠账一次清干净,之后切到增量校验,只用高水位线对比最近变更过的记录,增量校验的触发方式可以是定时任务,也可以是监听binlog后触发校验,两者配合,能把校验损耗降得很低,又能保证持续发现差异。
常见问题与踩坑:别让双写本身变成事故
双写校验不是银弹,操作不当反而会制造新问题。
主键冲突与唯一索引
新旧库都使用自增主键时,迁移后的新库主键可能已经偏移,双写时很容易撞主键,解决办法是给新库设置自增偏移量,或者用业务唯一键代替自增主键,否则每次插入都报“Duplicate entry”,双写日志会被刷爆。

事务与回滚的边界
跨库事务是个坑,不要指望一个本地事务能同时提交新旧库,更不要写“先写旧库,再写新库,新库失败就回滚旧库”的代码,因为回滚只对旧库有效,新库已经埋了一条脏数据,正确思路是允许新库暂时不一致,靠校验和补偿来收敛。
脏数据被“二次放大”
如果旧库本身就存在脏数据,双写会把这些脏数据原样复制到新库,等于问题被放大了一倍,迁移之前先做一次数据清洗,把重复记录、非法枚举值、失效外键处理干净,再开始双写,校验脚本本身也要先在测试库上跑通,避免因为拼接顺序错乱而产生误报。
数据迁移双写校验怎么做:常见问题解答
数据迁移双写校验怎么做才不卡业务?
把校验任务拆成小批量分片,每个分片只处理几千条记录,通过调整线程数控制数据库压力,全量校验放在凌晨低峰期,白天只跑增量校验,增量校验基于高水位线,只检查最近变更过的记录,对业务影响很小。
新旧数据库记录不一致怎么办,先修哪边?
先分清差异类型,旧库有而新库没有,用旧库补齐;两边都有但字段值不同,先查变更日志和双写日志,判断哪一边是最近更新的记录,一般情况下以旧库为准,因为旧库还在承接核心写入,如果旧库已经切为只读,文档明确规定以新库为准,那就反向修复,修复完后立即重跑该分片校验,确认补的数据没有再次偏移。
双写校验要跑多久才能停?
建议至少持续一到两周,迁移后的前三天是差异高发期,业务写入路径会不断触碰边界,当连续多个校验周期(比如连续72小时)差异数为0,并且业务流量已经全部切到新库、旧库进入只读状态,就可以关闭双写,旧库不要马上删除,保留一段时间做最终备份。
数据迁移没有“一迁了之”的轻松,双写校验不会让迁移变快,但它能让新旧库的账目始终对得上,把双写加校验这套机制跑起来,你就有了随时纠正偏差的底气。