Skip to content

第 9 讲:分库分表原理、策略与常见问题

核心结论(10 条必记)

  1. 分库分表是最后手段 -- 先考虑索引优化、SQL 优化、读写分离、缓存,都用过还不够再分
  2. 垂直拆分解决业务隔离,水平拆分解决数据量 -- 两种方式解决的问题不同
  3. 分片键要保证数据分布均匀、覆盖常见查询、字段值稳定 -- 选错分片键是最常见的坑
  4. 不要用时间做分片键 -- 热点严重,当前月表写入压力极大,数据分布极不均匀
  5. 全局 ID 推荐雪花算法或号段模式 -- UUID 无序导致 B+ 树页分裂,自增主键分片后冲突
  6. 分库分表后分页、join、聚合都变复杂 -- 带分片键可直接路由,不带则代价高
  7. 非分片键查询需要路由索引或全分片扫描 -- 在 order_no 里编码 user_id 是常见技巧
  8. 扩容推荐翻倍扩容或一致性 Hash -- 提前规划逻辑分片数,物理扩容只迁移部分数据
  9. 分布式事务大多数场景用最终一致性 + 消息队列 -- 2PC 性能差,TCC 业务侵入强
  10. 唯一约束在分库分表后失效 -- 单分片内唯一不等于全局唯一,需要业务层或发号器保证

一、什么时候需要分库分表?

不是所有系统都需要分库分表。

单库单表的瓶颈

瓶颈表现关注阈值
数据量太大即使有索引查询也慢单表超 1000 万行关注,5000 万行以上明显下降
写入并发太高单机扛不住每秒写入几万条以上
存储容量不够单机磁盘空间有限取决于硬件
单点风险单库宕机影响整个服务任何规模都存在

分库分表之前先考虑这些

  1. 加索引
  2. 优化 SQL
  3. 读写分离
  4. 加缓存(Redis)
  5. 垂直拆分(先分表)

这些手段都用过还不够,再考虑水平分库分表。


二、两种拆分方式

1. 垂直拆分

垂直分库:按业务把不同的表拆到不同的库

原来一个库:
- 用户表、订单表、商品表、库存表、支付表

拆成:
- 用户库:用户表
- 订单库:订单表
- 商品库:商品表、库存表
- 支付库:支付表

垂直分表:把一张宽表拆成多张窄表

sql
-- 核心表(高频访问)
user_base(id, name, phone, email, create_time)

-- 扩展表(低频访问)
user_detail(user_id, avatar, intro, address)
维度优点缺点
业务隔离每个库/表更聚焦跨库 join 变难
单库压力减少更容易维护没解决单表数据量大的问题

2. 水平拆分

把同一张表的数据按规则分散到多个库或多个表。

  • 水平分表order_info_0order_info_1order_info_2(同一个库)
  • 水平分库order_db_0order_db_1(不同库)
维度优点缺点
解决单表数据量真正有效跨库/表查询复杂
分散写入压力可横向扩展分布式事务问题
全局主键、扩容成本高

三、分片键怎么选?

水平拆分最核心的问题:按什么字段来决定一条数据放哪个库/表?

分片键选择原则

  1. 数据分布均匀 -- 不能全集中在某个分片
  2. 能覆盖最多的查询场景 -- 常见查询都带分片键,可直接路由
  3. 字段值稳定 -- 分片键变化需要迁移数据

常见场景的分片键

分片键好处注意点
订单表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:分页查询

不带分片键:所有分片各自查,内存中合并排序分页。代价随页数增大而增大,深翻页很慢。

解决方案

  1. 带分片键查询 -- 直接路由到对应分片
  2. 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:跨库统计/聚合

解决方案

  1. 汇总各分片 -- 每个分片各自统计,应用层求和
  2. 数据仓库 -- MySQL -> Kafka -> ClickHouse 做聚合

问题4:非分片键查询

分片键是 user_id,但要按 order_no 查订单:

方案做法
全分片扫描查所有分片合并,代价高
路由索引维护 order_route(order_no, user_id) 映射表
order_no 编码 user_idorder_no = shard_id + timestamp + sequence,从订单号可知分片

七、扩容怎么做?

原来 4 个分片扩到 8 个,Hash 取模几乎所有数据都要迁移。

方案做法特点
翻倍扩容4 -> 8 -> 16,每个分片只有一半数据需迁移简单有效
一致性 Hash扩容时只有少量数据迁移需要引入一致性 Hash 库
提前规划分 1024 个逻辑分片,物理库只有 4 个,扩容迁移部分逻辑分片路由规则不变

八、分布式事务

单机事务无法跨库保证原子性。

方案对比

方案原理优点缺点适用场景
2PC两阶段提交强一致性能差、阻塞几乎不用
TCCTry-Confirm-Cancel灵活业务侵入强、需处理空回滚/幂等资金类
最终一致性消息队列 + 补偿性能好、扩展性强短暂不一致互联网业务主流

最终一致性示例:下单扣库存

1. 创建订单(主库提交)
2. 发消息到消息队列
3. 库存服务消费消息,扣减库存
4. 如果失败,重试或回滚订单

大多数互联网业务选择最终一致性 + 消息队列。 只有金融核心账务才需要强一致方案。


九、常见坑点

问题
分片键选错按时间分片导致热点
全局 ID 重复没用全局发号器
深翻页没处理全分片扫描性能极差
跨库 join 没设计好查询要做全分片扫描
分布式事务用 2PC性能差
扩容没提前规划大量数据迁移影响线上
唯一约束失效分片内唯一不等于全局唯一

十、面试高频题

1. 为什么需要分库分表?

单库单表在数据量大、并发高时有性能瓶颈。分库分表可以分散数据量、分散写入压力、横向扩展。但复杂度高,应该在其他优化手段都用过之后再考虑。

2. 分片键怎么选?

要保证数据分布均匀、能覆盖常见查询、字段值稳定。常见如订单表按 user_id 分片。

3. 分库分表后怎么做分页?

带分片键可直接路由到对应分片分页。不带则需全分片查询合并,代价高。可以考虑用 ES 做分页。

4. 全局主键怎么生成?

常用雪花算法(趋势有序、高性能、本地生成)或号段模式(简单可靠)。

5. 分布式事务怎么处理?

大多数业务用最终一致性 + 消息队列。性能要求高可用 TCC。一般不用 2PC。


练习题

  • [ ] 练习1:电商平台订单表 5 亿条数据查询很慢,设计一个分库分表方案:选什么分片键?什么路由策略?按订单号查询怎么处理?
  • [ ] 练习2:分库分表后,查询某个用户最近 20 条订单按时间倒序,这个查询有没有问题?怎么处理?
  • [ ] 练习3:为什么唯一索引在分库分表后会失效?举个例子说明。

基于 VitePress 构建