Skip to content

分库分表扩容实战:双写、数据迁移与不停机切流

本文是分布式系统系列的第 24 篇,侧重工程实践。前置参考:31. 数据分片与一致性哈希04. 一致性哈希。 适合 P7/P8 面试场景:怎么设计一个不停机扩容方案。

分库分表之后,扩容才是真挑战

"分库分表"四个字听起来就是建表时设个分片键、配个 ShardingSphere 完事。但业务跑半年后,数据量翻了三倍,原来 4 个库扛不住了——这时候的扩容才是真正的工程难题。

一致性哈希在缓存层挺好用,MySQL 分库分表场景下却不太顶用。分库分表用的不是 key 映射到虚拟节点的模式,而是硬编码的库表路由规则。比如 order_id % 4 决定去哪个库,扩容到 8 个库后变成 % 8,绝大多数已有数据的路由结果都变了——这不是"迁一部分数据"的问题,是几乎全量数据都要搬。

三种扩容思路,各有各的代价

双倍扩容:取模公式的 trick

把 4 库扩成 8 库,取模公式从 % 4 变成 % 8。但有个数学规律可以利用:hash(key) % 8 的结果要么和 % 4 一样,要么等于 % 4 + 4

比如 hash("order_100") % 4 = 2% 8 的结果要么是 2 要么是 6。这意味着每个旧库的数据只需要拆成两半,一半留在原库,一半迁到新库,不需要跨库迁移

这个 trick 对 n → 2n 的扩容有效,但跳着扩就不行(4→12 没法复用)。而且数据库层面"拆成两半"不是读一半数据贴标签就能做的——你得按分片键范围或 hash 值把数据筛出来,耗时长、IO 重。

双写迁移:最稳妥但最复杂

生产环境用得最多的方案:新旧两套分片同时接受写入,等数据一致后再切流

状态机大概是这样的:

text
第一阶段:双写 + 全量迁移
  ┌──────────────┐    ┌──────────────┐
  │  应用层      │    │              │
  │  写旧库      │    │  写新库      │
  │  读旧库      │    │  (异步开始)  │
  └──────┬───────┘    └──────────────┘
         │                    ▲
         │           ┌────────┴────────┐
         │           │  全量迁移进程    │
         │           │  SELECT 旧库    │
         │           │  → INSERT 新库  │
         │           └─────────────────┘


  ┌──────────────┐
  │  旧分片库     │
  └──────────────┘

第二阶段:增量追平 + 校验
  ┌──────────────┐    ┌──────────────┐
  │  应用层      │    │              │
  │  写旧库      │    │  写新库      │
  │  读旧库      │    │  (继续写入)  │
  └──────┬───────┘    └──────────────┘
           │                 │
           └────────┬────────┘

            ┌──────────────────┐
            │  增量校验进程     │
            │  checksum 比对   │
            │  + 修复差异行    │
            └──────────────────┘

第三阶段:切读 + 灰度
  ┌──────────────┐    ┌──────────────┐
  │  应用层      │    │              │
  │  写双库      │    │  读新库      │
  │  (灰度 1%)   │    │  (灰度 1%)   │
  └──────────────┘    └──────────────┘

第四阶段:摘旧写
  ┌──────────────┐    ┌──────────────┐
  │  应用层      │    │              │
  │  写新库      │    │  读新库      │
  │  (全量)      │    │  (全量)      │
  └──────────────┘    └──────────────┘

每个阶段怎么切、切多少、出问题怎么回滚,是这套方案的核心设计。

binlog 订阅迁移:降低对应用层的侵入

双写需要在应用层改代码——每条写操作都要多写一份。如果应用代码不好改(比如第三方系统),或者不想在业务代码里掺这种逻辑,可以用 binlog 订阅。

思路:消费旧库的 binlog,解析成 SQL 然后回放到新库。Canal、Otter、DRC 都是干这个的。

应用层无感知,代价是:

  • binlog 和 SQL 的映射不是 1:1。一条 INSERT INTO ... VALUES (...) 可能刚好对应新库的某个分片,但 UPDATE ... WHERE 如果没带分片键,要在新库全分片扫一遍。
  • 延迟不可控。binlog 消费端如果挂了,新库的数据会落后,切流窗口期拉长。
  • DDL 同步是噩梦。旧库加字段 → binlog 里有 DDL → 新库也得加,但分片后的表结构可能不完全一样。

迁移期间的一致性问题

双写方案最核心的坑是:应用层双写不能保证原子性。写旧库成功、写新库失败,两边数据就对不上了。

解决方案是"校验 + 补偿":

sql
-- 阶段一:全量迁移对每条记录做 checksum
SELECT id, CRC32(CONCAT(col1, col2, ...)) AS checksum 
FROM old_order_0;

-- 阶段二:增量校验,比对每分钟的写入
-- 新库侧执行同样的行
SELECT id, CRC32(CONCAT(col1, col2, ...)) AS checksum 
FROM new_order_0;

-- 发现差异 → 以旧库为准修复新库

校验是个工程活。全量校验通常用 CRC32MD5 对整行做 hash,比对两边。但千万级数据量下,全量校验跑几个小时很正常,这期间增量写入还在继续。所以校验必须是分段进行的:先比对旧数据,再比对增量窗口。

更实用的做法是:定时跑增量校验任务,只校验最近一段时间(比如 5 分钟)内发生过变更的数据。通过业务字段(update_time)或 binlog 位点来圈定范围。

灰度切流:从 1% 到 100% 的决策树

数据追平到可接受程度后,开始切读流量。不是啪的一下全切,而是按用户维度灰度:

yaml
# 灰度路由配置
gray_rules:
  - uid_mod: 0           # uid 尾号为 0
    read_source: new     # 读新库
    write_source: both   # 写双库
  - uid_mod: 1-19        # uid 尾号 1-19
    read_source: old     # 读旧库
    write_source: both   # 写双库
  - uid_mod: 20-99       # 其余
    read_source: old
    write_source: old

灰度比例递增:1% → 5% → 20% → 50% → 100%。每个阶段观察多久?看业务容忍度,一般至少观察 1-2 个业务高峰周期。

灰度的回滚要跟灰度一样快。如果 20% 切流后发现问题,路由配置回滚到 5% 甚至 1%,数据层面不需要回滚——因为双写一直在跑,新库的数据是完整的,只是读流量切多切少的问题。

什么时候该摘旧写

灰度切读 100% 后,旧库还在接收写入。这一步是安全冗余,但也意味着维护成本翻倍(两套存储都要管)。

摘旧写的时机:

  • 新库持续运行 1-2 周,无异常报警
  • 数据一致性校验连续 3 天零差异
  • 旧库的写入延迟监控稳定(没有因为写双库导致的性能退化)

摘的时候,摘的是"写旧库",不是"删旧库"。旧库保留只读一段时间(比如 1 个月),作为最后的回滚选项。

与 ShardingSphere 弹性扩容的关系

ShardingSphere 5.x 的弹性扩容(scaling)内置了这套逻辑:数据搬迁、binlog 同步、一致性校验。但它简化了灰度切流部分——默认是"搬完数据后一次性切",对延迟敏感的业务不够友好。

如果自己实现的方案比 ShardingSphere 的 scaling 更灵活,无非是两点:

  • 灰度逻辑是自己的:路由规则放在配置中心,实时刷新,不需要重启应用
  • 校验粒度更细:按分片键范围拆成小任务,逐个校验,并行执行,随时可观察进度

总结

分库分表扩容,80% 的难度不在技术方案,在可观测性和回滚能力。方案好不好,不在能不能跑通,在跑到一半断电了怎么恢复、灰度到 50% 发现有问题怎么回退、校验发现数据不一致怎么修复。

面试问到这个话题,把迁移时序状态机、灰度比例递增策略、校验补偿机制讲清楚,比堆方案名称更有用。

参考

  • ShardingSphere Scaling 源码分析
  • Canal 原理:binlog 订阅与解析
  • Otter 双向同步与一致性保证
  • 《数据密集型应用系统设计》第 6 章:分区与再平衡

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1