分库与分表带来的分布式困境与应对之策
数据迁移与扩容问题
前面介绍到水平分表策略归纳总结为随机分表和连续分表两种情况。连续分表有可能存在数据热点的问题,有些表可能会被频繁地查询从而造成较大压力,热数据的表就成为了整个库的瓶颈,而有些表可能存的是历史数据,很少需要被查询到。连续分表的另外一个好处在于比较容易,不需要考虑迁移旧的数据,只需要添加分表就可以自动扩容。随机分表的数据相对比较均匀,不容易出现热点和并发访问的瓶颈。但是,分表扩展需要迁移旧的数据。
针对于水平分表的设计至关重要,需要评估中短期内业务的增长速度,对当前的数据量进行容量规划,综合成本因素,推算出大概需要多少分片。对于数据迁移的问题,一般做法是通过程序先读出数据,然后按照指定的分表策略再将数据写入到各个分表中。
表关联问题
在单库单表的情况下,联合查询是非常容易的。但是,随着分库与分表的演变,联合查询就遇到跨库关联和跨表关系问题。在设计之初就应该尽量避免联合查询,可以通过程序中进行拼装,或者通过反范式化设计进行规避。
分页与排序问题
一般情况下,列表分页时需要按照指定字段进行排序。在单库单表的情况下,分页和排序也是非常容易的。但是,随着分库与分表的演变,也会遇到跨库排序和跨表排序问题。为了最终结果的准确性,需要在不同的分表中将数据进行排序并返回,并将不同分表返回的结果集进行汇总和再次排序,最后再返回给用户。
分布式事务问题
随着分库与分表的演变,一定会遇到分布式事务问题,那么如何保证数据的一致性就成为一个必须面对的问题。目前,分布式事务并没有很好的解决方案,难以满足数据强一致性,一般情况下,使存储数据尽可能达到用户一致,保证系统经过一段较短的时间的自我恢复和修正,数据最终达到一致。
分布式全局唯一 ID
在单库单表的情况下,直接使用数据库自增主键来生成主键ID,这样确实比较简单。在分库分表的环境中,数据分布在不同的分表上,不能再借助数据库自增长特性。需要使用全局唯一 ID,例如 UUID、GUID等。关于如何选择合适的全局唯一 ID,我会在后面的章节中进行介绍。
五大困境(迁移扩容、跨片关联、跨片分页、分布式事务、全局 ID)点名齐全,本文把每个困境的应对之策落到可执行方案级,并补两个原文未列的困境(跨片聚合统计、唯一性约束),形成完整的”分片代价清单+对策库”。
困境一:迁移与扩容
对策谱系:
- 双倍扩容法:N 片扩 2N 片,每片数据一半迁出(哈希模 2N 后重分布);迁移期双写或停写窗口二选一
- 不停机迁移三步曲:① 增量双写(新路由+旧路由同写)② 存量搬迁(后台程序按片扫旧写新+校验)③ 切读(灰度按用户切新路由)→ 停旧写;每步可回滚
- 预分片防扩容:设计期分片数定够(如 128/256 逻辑片),物理库按需增加=搬逻辑片而非重哈希(映射表路由)——今天的主流做法
- 一致性哈希:节点级扩缩容只迁移相邻区间,适合缓存分片,数据库分片少用(范围查询与热点控制弱)
困境二:跨片关联
对策按成本排序:
- 设计期规避:冗余字段(订单表存 user_name 快照)、反范式宽表
- 应用层拼装:先查主片拿 ID 集,再批量查关联片(两次单片查询代替 join)
- 异构索引:关联查询需求重的数据同步到 ES/宽表(Canal binlog 同步),查询走异构层
- 绑定表(binding tables):相同分片键的表同片存储(订单与订单明细同按 user_id 分),片内 join 保留——ShardingSphere 支持声明绑定表
困境三:跨片分页与排序
- 归并排序:各片取 top(offset+limit) 内存归并——offset 深时各片传输量爆炸(offset 1 万=每片传 1 万行)
- 游标法改造:分页改”上一页最大排序键”条件,各片只回 limit 行归并——深分页正解
- 禁止深翻+业务限深:产品层限制(只允许前 N 页/无限滚动)
- 复杂排序统计需求整体迁 ES(见困境二的异构方案)
困境四:跨片事务
对策谱系(按一致性强度):
- 避免:设计让事务落在单片内(同分片键的业务操作聚合)——最优解
- XA/2PC(Seata XA、Atomikos):强一致,性能差(同步阻塞+锁持有长)
- TCC(Seata TCC):业务侵入(Try/Confirm/Cancel 三接口),性能与一致平衡
- Saga/消息最终一致:本地事务+事件驱动补偿,最终一致,业务需接受窗口
- 对账兜底:任何最终一致方案都配周期对账修复差异
选型:资金类 TCC/XA,普通业务消息最终一致+对账,能单片则单片。
困境五:全局唯一 ID
自增主键失效后的方案:雪花(趋势递增+本地生成,注意时钟回拨)、号段模式(DB 发号器批量取号,Leaf)、UUID(无序伤索引,仅非主键场景)。要点回顾:主键要趋势递增保 B+Tree 尾插,业务单号与主键 ID 分离(见分布式主键篇)。
困境六(补充):跨片聚合统计
COUNT/SUM/GROUP BY 跨片=各片局部聚合+全局归并,大数据量下中间结果集爆炸。对策:
- 实时性要求低:离线数仓(binlog→数仓→报表)
- 实时性要求中:预聚合表(定时/事件驱动增量汇总)
- 实时性要求高:ClickHouse/Druid 等 OLAP 引擎承接分析查询
困境七(补充):唯一性约束
单片唯一索引无法保证全局唯一(同值落不同片)。对策:
- 唯一键含分片键(自然保证)
- 全局去重表(唯一键作主键的单片去重表,先插去重表再写业务片——引入跨片事务则回到困境四)
- 业务层幂等键+补偿扫描兜底
困境-对策速查表
| 困境 | 首选对策 | 兜底 |
|---|---|---|
| 迁移扩容 | 预分片+搬逻辑片 | 双写三步曲迁移 |
| 跨片关联 | 冗余/绑定表 | ES 异构 |
| 跨片分页 | 游标归并 | 业务限深 |
| 跨片事务 | 单片化设计 | 消息最终一致+对账 |
| 全局 ID | 雪花/号段 | — |
| 跨片聚合 | 预聚合/OLAP | 离线数仓 |
| 全局唯一 | 唯一键含分片键 | 去重表+补偿 |
分片的五大(加二)困境本质是同一句话的不同投影:关系数据库的”单机一致性假设”被打破后,每个依赖该假设的能力(join/事务/自增/全表扫)都要重新发明。对策库的优先级永远一致:设计期规避 > 架构层替代(异构/预聚合)> 中间件补偿(Seata/归并)> 业务兜底(对账/限深)。带着这张优先级表做分片设计,困境就从”救火清单”变成”设计约束”。











