一、为什么需要分库分表
当单表数据逼近千万级、单库连接数被打满,或某个热点 Key 把整张表拖慢时,单纯堆硬件和加索引已触及天花板。分库分表(Sharding)把一个逻辑库/表拆成多个物理分片,分散存储与访问压力,是支撑高并发、海量数据的经典解法,核心在于选对分片键。
它通常出现在三个阶段都已用尽之后:读已用尽缓存、写已用尽主从复制、单表已超亿行。这时再不拆,慢查询会拖垮整库——可参考我们的PostgreSQL 慢查询实战与MySQL 索引底层原理来排查单表瓶颈。
二、垂直拆分:先按业务与字段解耦
2.1 垂直分库
按业务域把不同表拆到独立数据库,例如把订单、用户、商品分到三个库。好处是业务解耦、故障隔离、可按业务独立扩容,但跨库 JOIN 需要应用层组装。
2.2 垂直分表
把一张宽表按访问频率拆开:热字段(如标题、状态)留在主表,冷字段(如详情、日志、大文本)移到扩展表。这能减少大字段对热查询的拖累,也和数据库设计规范里的字段范式思路一致。
垂直拆分并不解决单表行数爆炸,但能显著降低单库体积、解耦业务、提升缓存命中率,是分库分表的前置第一步。
三、水平分片:把同一张表拆到多库多表
水平分片(Horizontal Sharding)按某个维度把行均匀分布到 N 个分片(如 order_0 ~ order_7)。目标是让「同一份查询尽量落在一个分片」,避免跨分片 JOIN 与全分片扫描。下面是一段最朴素的取模路由代码:
def shard_of(order_id, shard_count=8):
# 分片键 order_id 取模,映射到 0..shard_count-1
return order_id % shard_count
def route_table(order_id):
s = shard_of(order_id)
return f"order_{s}" # 物理表名 order_0 ... order_7
print(route_table(1001)) # -> order_1
注意:路由必须稳定且可复现,所有读写都按同一规则计算分片,否则会读不到数据。本地起一套分片环境可以用Docker 实战快速拉起多实例。
四、分片键(Sharding Key)的选择
4.1 好的分片键长什么样
高基数:取值足够多,才能均匀分散;低倾斜:不能让某个值占绝大多数(如「性别」只有两个值,极度倾斜);业务内聚:查询条件常带该字段,使大多数请求只命中一个分片。
4.2 常见反例
用自增主键当分片键:新数据全落在最后一个分片,形成写入热点;用低基数字段(如状态、地区过少):分片严重不均;用会被修改的字段:分片键一旦变更,整行要跨分片迁移,代价极高。
五、分片算法对比
| 算法 | 路由方式 | 扩容成本 | 热点表现 |
| 取模分片 | key % N | 高(需全量重分布) | 均匀但难扩容 |
| 范围分片 | 按区间分段 | 低(加段即可) | 易产生冷热倾斜 |
| 一致性哈希 | 哈希环定位节点 | 中(仅迁移相邻) | 均衡且易扩缩容 |
一致性哈希在节点增减时只迁移少量数据,是大规模集群的常用选择。其本质是先把节点和 Key 都哈希到一个环上,再顺时针找最近节点:
import hashlib
class ConsistentHash:
def __init__(self, nodes, repl=100):
self.ring = {}
for n in nodes:
for i in range(repl):
h = hashlib.md5(f"{n}#{i}".encode()).hexdigest()
self.ring[h] = n
self.keys = sorted(self.ring)
def get(self, key):
h = hashlib.md5(key.encode()).hexdigest()
for k in self.keys:
if h <= k:
return self.ring[k]
return self.ring[self.keys[0]]
ch = ConsistentHash(['db0','db1','db2'])
print(ch.get('order_1001'))
六、全局唯一 ID:Snowflake 思路
分片后数据库自增 ID 会冲突,需要全局唯一且趋势递增的 ID。Snowflake 把 64 位划分为「时间戳 + 机器号 + 序列号」,单机每毫秒可生成大量不重复 ID:
# Snowflake 64 位: 1位符号 | 41位毫秒时间戳 | 10位机器 | 12位序列
import time
class Snowflake:
def __init__(self, machine_id, epoch=1700000000000):
self.mid = machine_id
self.epoch = epoch
self.seq = 0
self.last = 0
def next(self):
now = int(time.time()*1000) - self.epoch
if now == self.last:
self.seq = (self.seq + 1) & 0xFFF
else:
self.seq = 0
self.last = now
return (now << 22) | (self.mid << 12) | self.seq
sf = Snowflake(machine_id=1)
print(sf.next())
若不想自建,也可借助号段模式或 Redis 原子自增(见Redis 实战)集中发号,但要评估中心节点的可用性。
七、跨分片查询与分布式事务痛点
最棘手的是跨分片聚合:分页、排序、JOIN、COUNT 都要把各分片结果拉回应用层再合并,性能随分片数线性下降。分布式事务更是难点,通常退而求其次采用最终一致:本地事务 + 可靠消息,或只用单分片事务。
# 跨分片聚合示例:先并发查各分片,再在应用层归并
totals = {}
for shard in range(8):
rows = query(f"SELECT SUM(amount) FROM order_{shard} WHERE day='2026-10-20'")
totals[shard] = rows[0]
grand_total = sum(totals.values()) # 应用层合并各分片结果
连接池在分片架构下尤其重要,每个分片都要独立管理连接,建议结合HikariCP 连接池调优控制资源上限。
八、平滑迁移:双写与灰度
从单表迁到分片不能停机。常用「双写 + 校验 + 灰度切读」:写请求同时落旧表和新分片,跑对账任务修复差异,确认一致后逐步把读流量切到分片,最后下线旧表。
# 双写迁移(伪代码)
def create_order(order):
write_legacy(order) # 旧单表,保底
shard = route_table(order.id)
write_shard(shard, order) # 新分片
enqueue_check(order.id) # 异步对账,不一致则修复
迁移与回滚脚本建议纳入GitHub Actions 流水线做自动化校验,并在VS Code里用远程开发确认线上环境。
九、落地清单与常见坑
落地前对照清单:① 是否真的到了必须拆的阶段(先确认慢查询与连接数瓶颈);② 分片键是否高基数、低倾斜、不可变;③ 跨分片查询是否有降级方案;④ 全局 ID 方案是否就绪;⑤ 迁移是否支持回滚。常见坑:分片键选错导致热点、取模分片扩容需全量迁移、跨分片事务强行用 2PC 拖垮性能。
十、小结
分库分表不是银弹,它是单库触顶后的工程权衡:用复杂度换容量与并发。先垂直拆分解耦业务,再用水平分片分散数据,全程围绕分片键做文章,配合全局 ID、双写迁移与严格对账,才能把海量数据稳稳托住。
十一、容量规划与监控怎么做
拆分之后容量规划要前置:先估算单分片上限(通常单表建议低于 2000 万行、单分片低于 500GB),再反推需要几个分片。监控重点也从「单库 QPS」转为「分片均衡度」与「热点分片」——一旦发现某分片明显倾斜,就要提前再平衡,避免木桶短板拖累整体。
| 指标 | 健康线 | 说明 |
| 单分片行数 | < 2000 万 | 超出需提前再分片 |
| 单分片大小 | < 500 GB | 控制备份与恢复时长 |
| 分片均衡度 | > 0.8 | 最大值/平均值,越接近 1 越均衡 |
| 跨分片查询比 | < 5% | 过高说明分片键选错 |
再平衡时务必配合双写与对账,避免数据漂移;相关迁移脚本同样建议接入持续集成做自动校验,并在每次扩容后回归一遍分片路由用例。




