面试题记录:在线数据迁移

一、分析

目的:在服务不停机、流量不断、数据持续变化的情况下,怎么保证 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 混用造成脏缓存

相关推荐
aXin_ya2 小时前
Redis 原理篇 (数据结构)
数据库·redis·缓存
2301_803538952 小时前
CSS如何设计简洁的移动端底部固定导航_利用position-fixed实现
jvm·数据库·python
木井巳2 小时前
【递归算法】组合总和
java·算法·leetcode·决策树·深度优先·剪枝
vegetablec2 小时前
CSS如何制作卡片翻开呈现另一面的翻牌动画
jvm·数据库·python
吕源林2 小时前
Golang怎么Redis发布订阅_Golang如何用Publish和Subscribe收发消息【实战】
jvm·数据库·python
redreamSo2 小时前
Turso:用 Rust 重写 SQLite,让数据库跑在每一个边缘节点
数据库·rust·sqlite
2301_764150562 小时前
Golang colly爬虫框架如何用_Golang colly教程【进阶】
jvm·数据库·python
2301_803538952 小时前
SQL统计各分组中排名前三的记录_使用窗口函数RANK
jvm·数据库·python
2301_782659182 小时前
如何让按钮悬停时阴影位置保持固定(仅按钮位移)
jvm·数据库·python