分库分表
什么时候才该分
先把顺序讲对,直接答「数据量大就分表」会被追问。优化手段按成本从低到高:
索引优化 / SQL 改写 → 加缓存 → 读写分离 → 垂直拆分 → 水平分表 → 水平分库
分库分表是最后手段,因为它会引入跨库 JOIN、分布式事务、全局 ID、跨库分页等一整套新问题。
常见的触发阈值(经验值,不是硬标准):
- 单表行数超过 1000 万~2000 万,或单表容量超过 20GB
- 本质原因是 B+ 树层数增加:三层树约能存 2000 万行,再多就变四层,每次查询多一次磁盘 IO
- 另一个常被忽略的点:大表的 DDL、备份、恢复时间变得不可接受
阿里规范的说法是「单表行数超过 500 万行或单表容量超过 2GB 才推荐分库分表」—— 注意它的前半句是「如果预计三年后数据量才达到这个级别,请不要在创建表时就分库分表」。
四种拆分方式
| 方式 | 怎么拆 | 解决什么 |
|---|---|---|
| 垂直分库 | 按业务域拆成不同的库(用户库、订单库、商品库) | 业务解耦、分散单库连接数和 IO 压力 |
| 垂直分表 | 按字段冷热拆成主表 + 扩展表 | 单页装更多行,高频查询变快 |
| 水平分表 | 同一个库里拆成 order_0 ~ order_15 | 降低单表数据量(但单机 IO / CPU 仍是瓶颈) |
| 水平分库 | 拆到不同的库 / 实例 | 真正的容量和性能水平扩展 |
实践中通常是水平分库 + 分表组合:如 4 个库 × 每库 8 张表 = 32 个分片。好处是扩容时可以先在库内调整。
分片策略
范围分片(Range)
按 ID 区间或时间区间划分(id 1~1000万 进表 0)。
- ✅ 扩容简单(加新区间即可)、范围查询高效
- ❌ 热点集中 —— 新数据全部写入最后一个分片,写压力不均
按时间分片是范围分片的特例,适合日志、订单归档这类有明显时间局部性的场景(老数据可以直接归档整表)。
哈希分片(Hash)
shard = hash(key) % N。
- ✅ 数据分布均匀、无写热点
- ❌ 扩容需要大规模数据迁移 —— N 从 4 变 8,几乎所有数据的归属都变了
缓解扩容问题的两个技巧:
- 分片数取 2 的幂次 —— 从 4 个分片扩到 8 个时,
hash % 8的结果要么等于原值、要么等于「原值 + 4」,每个分片只需迁出一半数据,且目标确定 - 一致性哈希 —— 引入哈希环和虚拟节点,增减节点只影响环上相邻的一段数据
双写迁移(生产标准做法):新老分片规则同时写 → 历史数据后台双写校验 → 读流量灰度切到新规则 → 确认无误后下线老规则。全程可回滚。
分片键怎么选
最重要的决策,选错了后面全是补救。四条标准:
- 高频查询条件必须包含它 —— 否则查询要广播到所有分片再聚合,性能灾难
- 分布均匀 —— 不能导致某个分片远大于其他(如用「商家 ID」分片,头部商家的数据会撑爆一个分片)
- 不可变 —— 分片键的值变了意味着数据要在分片间搬迁
- 基数足够大 —— 用「状态」这种只有几个值的字段分片,数据只会落到少数分片
经典难题:订单表按什么分片? 用户要查「我的订单」,商家要查「我店铺的订单」,两个维度冲突。三种解法:
- 按用户 ID 分片 + 异构索引表 —— 另建一张「商家ID → 订单ID」的映射表(或者同步到 ES),商家查询先查映射再回查
- 双写(基因法 / 冗余表) —— 同一份订单按两个维度各存一份,用户维度和商家维度各一套分片
- 分片键里嵌入基因 —— 把用户 ID 的若干位哈希值「种」进订单 ID,这样用订单 ID 也能算出它在哪个分片,解决「按订单号查询」
引入的四大问题
1. 全局唯一 ID
自增主键在多库下会冲突。方案见表设计与范式的主键设计。常用雪花算法(趋势递增,对 B+ 树友好)或号段模式。
雪花算法的时钟回拨问题:机器时钟被 NTP 校正往回拨,可能生成重复 ID。应对是检测到回拨时拒绝发号并告警,或用上次的时间戳继续递增。
2. 跨库 JOIN
直接做不了。四种替代:
- 字段冗余 —— 订单表里直接存
用户昵称,避免关联用户表 - 应用层组装 —— 分别查两个库,在内存里关联(注意 N+1 问题,要批量
IN查询) - 全局表 / 广播表 —— 字典表、配置表这类数据量小且很少变的表,在每个库里存一份完整副本
- 宽表 / 数据同步 —— 通过 binlog 同步到 ES 或数仓,复杂查询走那边
3. 分布式事务
跨库的写操作失去了本地事务保证。方案按一致性强度:
| 方案 | 一致性 | 说明 |
|---|---|---|
| 2PC / XA | 强一致 | 性能差、协调者单点,MySQL 对 XA 的支持也有坑,生产少用 |
| TCC | 最终一致 | Try-Confirm-Cancel,业务侵入大,每个服务要实现三个接口 |
| 本地消息表 / 事务消息 | 最终一致 | 把消息和业务数据写进同一个本地事务,再异步投递。实现简单可靠,最常用 |
| Saga | 最终一致 | 长流程,每步都有补偿操作 |
| 最大努力通知 + 对账 | 弱 | 定时任务兜底修复,几乎所有金融系统都有这一层 |
实践共识:优先设计成不需要分布式事务(把需要一起成功的数据放进同一个分片),实在需要就用本地消息表 + 幂等 + 对账。
4. 跨库分页与排序
ORDER BY time LIMIT 10000, 10 在 N 个分片上无法简单拆分 —— 不能每个分片取 LIMIT 10000,10 然后合并,因为全局第 10000~10010 条可能全部来自某一个分片。
| 方案 | 做法 | 代价 |
|---|---|---|
| 全局查询法 | 每个分片都取 LIMIT 0, 10010,内存归并后取需要的 10 条 | 页码越深、分片越多,开销爆炸 |
| 禁止跳页 | 只提供「下一页」,用上一页最后一条的值做游标 | 体验受限,但性能恒定(推荐) |
| 二次查询法 | 先按 offset/N 在各分片查,找到全局最小值,再用该值回各分片精确定位 | 实现复杂,但精确且高效 |
| 业务妥协 | 限制最大页数(如只能翻 100 页) | 最简单,搜索类产品普遍采用 |
| 走 ES | 复杂分页排序交给搜索引擎 | 引入新组件和同步延迟 |
跨库 COUNT 和聚合同理:COUNT 可以各分片求和;AVG 不能直接平均(要分别取 SUM 和 COUNT 再算);DISTINCT 和 GROUP BY 需要把中间结果全部拉回来归并。
中间件
| 中间件 | 形态 | 特点 |
|---|---|---|
| ShardingSphere-JDBC | 客户端 SDK | 无额外网络跳转、性能好,但只支持 Java 且需要改应用 |
| ShardingSphere-Proxy | 代理 | 伪装成 MySQL,任何语言和客户端都能连,多一跳网络 |
| MyCat | 代理 | 曾经流行,社区活跃度已下降 |
| Vitess | 代理 | YouTube 开源、CNCF 项目,K8s 原生,超大规模验证过 |
新项目的另一个选择是直接上分布式数据库(TiDB、OceanBase、PolarDB-X)—— 它们在存储层自动分片,对应用几乎透明,省掉了上面大半的问题。代价是运维复杂度、资源成本,以及部分 SQL 特性和隔离级别的差异。被问「为什么不直接用 TiDB」时,答这个取舍比答「没考虑过」好得多。