第 9 讲:分库分表原理、策略与常见问题
核心结论(10 条必记)
- 分库分表是最后手段 -- 先考虑索引优化、SQL 优化、读写分离、缓存,都用过还不够再分
- 垂直拆分解决业务隔离,水平拆分解决数据量 -- 两种方式解决的问题不同
- 分片键要保证数据分布均匀、覆盖常见查询、字段值稳定 -- 选错分片键是最常见的坑
- 不要用时间做分片键 -- 热点严重,当前月表写入压力极大,数据分布极不均匀
- 全局 ID 推荐雪花算法或号段模式 -- UUID 无序导致 B+ 树页分裂,自增主键分片后冲突
- 分库分表后分页、join、聚合都变复杂 -- 带分片键可直接路由,不带则代价高
- 非分片键查询需要路由索引或全分片扫描 -- 在 order_no 里编码 user_id 是常见技巧
- 扩容推荐翻倍扩容或一致性 Hash -- 提前规划逻辑分片数,物理扩容只迁移部分数据
- 分布式事务大多数场景用最终一致性 + 消息队列 -- 2PC 性能差,TCC 业务侵入强
- 唯一约束在分库分表后失效 -- 单分片内唯一不等于全局唯一,需要业务层或发号器保证
一、什么时候需要分库分表?
不是所有系统都需要分库分表。
单库单表的瓶颈
| 瓶颈 | 表现 | 关注阈值 |
|---|---|---|
| 数据量太大 | 即使有索引查询也慢 | 单表超 1000 万行关注,5000 万行以上明显下降 |
| 写入并发太高 | 单机扛不住 | 每秒写入几万条以上 |
| 存储容量不够 | 单机磁盘空间有限 | 取决于硬件 |
| 单点风险 | 单库宕机影响整个服务 | 任何规模都存在 |
分库分表之前先考虑这些
- 加索引
- 优化 SQL
- 读写分离
- 加缓存(Redis)
- 垂直拆分(先分表)
这些手段都用过还不够,再考虑水平分库分表。
二、两种拆分方式
1. 垂直拆分
垂直分库:按业务把不同的表拆到不同的库
原来一个库:
- 用户表、订单表、商品表、库存表、支付表
拆成:
- 用户库:用户表
- 订单库:订单表
- 商品库:商品表、库存表
- 支付库:支付表垂直分表:把一张宽表拆成多张窄表
sql
-- 核心表(高频访问)
user_base(id, name, phone, email, create_time)
-- 扩展表(低频访问)
user_detail(user_id, avatar, intro, address)| 维度 | 优点 | 缺点 |
|---|---|---|
| 业务隔离 | 每个库/表更聚焦 | 跨库 join 变难 |
| 单库压力减少 | 更容易维护 | 没解决单表数据量大的问题 |
2. 水平拆分
把同一张表的数据按规则分散到多个库或多个表。
- 水平分表:
order_info_0、order_info_1、order_info_2(同一个库) - 水平分库:
order_db_0、order_db_1(不同库)
| 维度 | 优点 | 缺点 |
|---|---|---|
| 解决单表数据量 | 真正有效 | 跨库/表查询复杂 |
| 分散写入压力 | 可横向扩展 | 分布式事务问题 |
| 全局主键、扩容成本高 |
三、分片键怎么选?
水平拆分最核心的问题:按什么字段来决定一条数据放哪个库/表?
分片键选择原则
- 数据分布均匀 -- 不能全集中在某个分片
- 能覆盖最多的查询场景 -- 常见查询都带分片键,可直接路由
- 字段值稳定 -- 分片键变化需要迁移数据
常见场景的分片键
| 表 | 分片键 | 好处 | 注意点 |
|---|---|---|---|
| 订单表 | user_id | 同一用户订单在同一分片 | 按订单号查需要额外处理 |
| 用户表 | user_id | 分布均匀,查询都带 | -- |
| 消息表 | conversation_id | 同一会话消息在同一分片 | -- |
分片键选择常见坑
| 坑 | 问题 |
|---|---|
| 按时间分片 | 热点严重,当前月表压力极大,数据分布极不均匀 |
| 按地区分片 | 不同地区用户量差异大,容易出现热点分片 |
| 用会变的字段 | 用户换手机号就需要迁移数据 |
四、路由策略
1. Hash 取模
分片编号 = user_id % 分片数量
user_id = 100 -> 100 % 4 = 0 -> 分片 0
user_id = 101 -> 101 % 4 = 1 -> 分片 1
user_id = 102 -> 102 % 4 = 2 -> 分片 2
user_id = 103 -> 103 % 4 = 3 -> 分片 3| 优点 | 缺点 |
|---|---|
| 简单、数据分布均匀 | 扩容困难,分片数变了几乎所有数据都要迁移 |
2. 一致性 Hash
扩容时只有少量数据需要迁移。
3. 范围分片
user_id 1 ~ 1000000 -> 分片 0
user_id 1000001 ~ 2000000 -> 分片 1| 优点 | 缺点 |
|---|---|
| 扩容方便、范围查询效率高 | 新数据集中在最后一个分片,可能出现热点 |
4. 映射表
sql
route_table(user_id, shard_id)| 优点 | 缺点 |
|---|---|
| 灵活、可任意迁移 | 路由表本身会成为瓶颈 |
五、全局主键怎么生成?
分库分表后自增主键不够用:分片 0 的 ID 可能和分片 1 冲突。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| UUID | 全局唯一、简单 | 太长、无序(页分裂严重)、存储大 | 非索引场景 |
| 号段模式 | 有序、性能好、简单 | 发号器需要高可用 | 中小规模 |
| 雪花算法 | 趋势递增、高性能、本地生成 | 依赖时钟、时钟回拨需处理 | 生产推荐 |
| Redis 自增 | 高性能、有序 | 依赖 Redis 高可用 | 有 Redis 基础设施 |
雪花算法(Snowflake)ID 结构
1bit 41bit 10bit 12bit
符号位 时间戳 工作机器ID 序列号- 41bit 时间戳:可用约 69 年
- 10bit 机器 ID:最多 1024 台机器
- 12bit 序列号:每毫秒最多生成 4096 个 ID
号段模式
sql
CREATE TABLE id_generator(
biz_type VARCHAR(50) PRIMARY KEY COMMENT '业务类型',
max_id BIGINT NOT NULL COMMENT '当前最大ID',
step INT NOT NULL COMMENT '步长'
);每次批量取一段 ID(如 1000 个),在内存中使用,用完再取。
六、分库分表后的查询问题
问题1:分页查询
不带分片键:所有分片各自查,内存中合并排序分页。代价随页数增大而增大,深翻页很慢。
解决方案:
- 带分片键查询 -- 直接路由到对应分片
- ES 做分页 -- 数据同步到 Elasticsearch
问题2:跨库 JOIN
sql
-- 分库后无法直接 join
SELECT a.*, b.name FROM order_info a JOIN user b ON a.user_id = b.id;解决方案:
| 方案 | 做法 | 适用场景 |
|---|---|---|
| 业务层关联 | 先查一个表,再查另一个表,代码里关联 | 通用 |
| 冗余字段 | order_info 里冗余 user_name | 查询频繁的字段 |
| 宽表 | 提前整合到一张宽表 | 报表统计场景 |
问题3:跨库统计/聚合
解决方案:
- 汇总各分片 -- 每个分片各自统计,应用层求和
- 数据仓库 -- MySQL -> Kafka -> ClickHouse 做聚合
问题4:非分片键查询
分片键是 user_id,但要按 order_no 查订单:
| 方案 | 做法 |
|---|---|
| 全分片扫描 | 查所有分片合并,代价高 |
| 路由索引 | 维护 order_route(order_no, user_id) 映射表 |
| order_no 编码 user_id | order_no = shard_id + timestamp + sequence,从订单号可知分片 |
七、扩容怎么做?
原来 4 个分片扩到 8 个,Hash 取模几乎所有数据都要迁移。
| 方案 | 做法 | 特点 |
|---|---|---|
| 翻倍扩容 | 4 -> 8 -> 16,每个分片只有一半数据需迁移 | 简单有效 |
| 一致性 Hash | 扩容时只有少量数据迁移 | 需要引入一致性 Hash 库 |
| 提前规划 | 分 1024 个逻辑分片,物理库只有 4 个,扩容迁移部分逻辑分片 | 路由规则不变 |
八、分布式事务
单机事务无法跨库保证原子性。
方案对比
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 2PC | 两阶段提交 | 强一致 | 性能差、阻塞 | 几乎不用 |
| TCC | Try-Confirm-Cancel | 灵活 | 业务侵入强、需处理空回滚/幂等 | 资金类 |
| 最终一致性 | 消息队列 + 补偿 | 性能好、扩展性强 | 短暂不一致 | 互联网业务主流 |
最终一致性示例:下单扣库存
1. 创建订单(主库提交)
2. 发消息到消息队列
3. 库存服务消费消息,扣减库存
4. 如果失败,重试或回滚订单大多数互联网业务选择最终一致性 + 消息队列。 只有金融核心账务才需要强一致方案。
九、常见坑点
| 坑 | 问题 |
|---|---|
| 分片键选错 | 按时间分片导致热点 |
| 全局 ID 重复 | 没用全局发号器 |
| 深翻页没处理 | 全分片扫描性能极差 |
| 跨库 join 没设计好 | 查询要做全分片扫描 |
| 分布式事务用 2PC | 性能差 |
| 扩容没提前规划 | 大量数据迁移影响线上 |
| 唯一约束失效 | 分片内唯一不等于全局唯一 |
十、面试高频题
1. 为什么需要分库分表?
单库单表在数据量大、并发高时有性能瓶颈。分库分表可以分散数据量、分散写入压力、横向扩展。但复杂度高,应该在其他优化手段都用过之后再考虑。
2. 分片键怎么选?
要保证数据分布均匀、能覆盖常见查询、字段值稳定。常见如订单表按 user_id 分片。
3. 分库分表后怎么做分页?
带分片键可直接路由到对应分片分页。不带则需全分片查询合并,代价高。可以考虑用 ES 做分页。
4. 全局主键怎么生成?
常用雪花算法(趋势有序、高性能、本地生成)或号段模式(简单可靠)。
5. 分布式事务怎么处理?
大多数业务用最终一致性 + 消息队列。性能要求高可用 TCC。一般不用 2PC。
练习题
- [ ] 练习1:电商平台订单表 5 亿条数据查询很慢,设计一个分库分表方案:选什么分片键?什么路由策略?按订单号查询怎么处理?
- [ ] 练习2:分库分表后,查询某个用户最近 20 条订单按时间倒序,这个查询有没有问题?怎么处理?
- [ ] 练习3:为什么唯一索引在分库分表后会失效?举个例子说明。