跳到主要内容

分库分表

什么时候才该分​

先把顺序讲对,直接答「数据量大就分表」会被追问。优化手段按成本从低到高:

索引优化 / 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,几乎所有数据的归属都变了

缓解扩容问题的两个技巧:

  1. 分片数取 2 的幂次 —— 从 4 个分片扩到 8 个时,hash % 8 的结果要么等于原值、要么等于「原值 + 4」,每个分片只需迁出一半数据,且目标确定
  2. 一致性哈希 —— 引入哈希环和虚拟节点,增减节点只影响环上相邻的一段数据

双写迁移(生产标准做法):新老分片规则同时写 → 历史数据后台双写校验 → 读流量灰度切到新规则 → 确认无误后下线老规则。全程可回滚。

分片键怎么选​

最重要的决策,选错了后面全是补救。四条标准:

  1. 高频查询条件必须包含它 —— 否则查询要广播到所有分片再聚合,性能灾难
  2. 分布均匀 —— 不能导致某个分片远大于其他(如用「商家 ID」分片,头部商家的数据会撑爆一个分片)
  3. 不可变 —— 分片键的值变了意味着数据要在分片间搬迁
  4. 基数足够大 —— 用「状态」这种只有几个值的字段分片,数据只会落到少数分片

经典难题:订单表按什么分片? 用户要查「我的订单」,商家要查「我店铺的订单」,两个维度冲突。三种解法:

  • 按用户 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」时,答这个取舍比答「没考虑过」好得多。