跳到主要内容

数据迁移方案设计

问题

如何设计一个安全的数据库迁移方案?

答案

迁移场景

  • 单库 → 分库分表
  • MySQL → 新分片集群
  • 表结构变更(大表 DDL)
  • 数据中心迁移

双写迁移方案(推荐)

双写阶段 —— 写入新旧两个库
@Service
public class OrderService {
@Autowired private OrderOldMapper oldMapper;
@Autowired private OrderNewMapper newMapper;

@Transactional
public void createOrder(Order order) {
// 1. 写旧库(主)
oldMapper.insert(order);

// 2. 异步写新库
CompletableFuture.runAsync(() -> {
try {
newMapper.insert(order);
} catch (Exception e) {
// 写新库失败不影响主流程,记录差异后补偿
diffLogMapper.insert(new DiffLog(order.getId(), "INSERT_FAIL"));
}
});
}
}

历史数据迁移

增量迁移 —— 分批处理
public void migrateHistory() {
long lastId = 0;
int batchSize = 1000;

while (true) {
// 分批查询,避免一次加载过多
List<Order> batch = oldMapper.findByIdGreaterThan(lastId, batchSize);
if (batch.isEmpty()) break;

// 批量写入新库
newMapper.batchInsert(batch);

lastId = batch.get(batch.size() - 1).getId();
log.info("已迁移到 ID: {}", lastId);
}
}

数据校验

抽样校验 + 全量校验
@Scheduled(cron = "0 */10 * * * ?")
public void dataVerify() {
// 全量比对总数
long oldCount = oldMapper.count();
long newCount = newMapper.count();
if (oldCount != newCount) {
alertService.send("数据量不一致: old=" + oldCount + ", new=" + newCount);
}

// 抽样比对内容(随机取 100 条比较)
List<Long> sampleIds = oldMapper.randomIds(100);
for (Long id : sampleIds) {
Order oldOrder = oldMapper.findById(id);
Order newOrder = newMapper.findById(id);
if (!Objects.equals(oldOrder, newOrder)) {
diffLogMapper.insert(new DiffLog(id, "CONTENT_DIFF"));
}
}
}

常见面试问题

Q1: 迁移过程中如何保证数据不丢?

答案

  • 双写期间以旧库为主,新库写入失败记录差异日志
  • 历史数据迁移完成后做全量数据校验
  • 差异数据通过补偿任务修复

Q2: 何时切流量到新库?

答案

  • 历史数据迁移 100% 完成
  • 增量双写数据一致性校验通过
  • 灰度切 10% 读流量到新库,观察无异常后逐步放量

Q3: 大表 DDL 怎么做?

答案

  • pt-online-schema-change:创建新表 → 触发器同步 → 交换表名
  • gh-ost:基于 binlog 的在线变更,不使用触发器
  • 直接 ALTER:5.6+ 支持 Online DDL,但大表仍可能锁表

相关链接