跳到主要内容

分库分表

问题

什么时候需要分库分表?垂直拆分和水平拆分有什么区别?常用的分片策略有哪些?分库分表后如何解决分布式 ID、跨库查询等问题?

答案

什么时候需要分库分表

问题表现解决方案
单表数据量过大单表超过 2000 万行,查询变慢水平分表
数据库连接数不够高并发时连接池耗尽分库
磁盘 I/O 瓶颈磁盘读写满载分库(分散 I/O)
单库数据量过大备份恢复时间过长分库
分库分表是最后手段

分库分表会引入大量复杂性,在此之前应先尝试:

  1. SQL 优化 + 索引优化
  2. 读写分离(主写从读)
  3. 缓存(Redis 热点数据)
  4. 归档历史数据(冷热分离)
  5. 升级硬件(SSD、增加内存)

只有以上方案都无法满足需求时才考虑分库分表。

拆分方式

垂直拆分

类型说明适用场景
垂直分库按业务模块拆分到不同数据库微服务架构,各服务独立数据库
垂直分表将大表按字段拆分(常用字段和不常用字段分开)表字段多且部分字段(如 TEXT/BLOB)很少查询

水平拆分

类型说明适用场景
水平分表同一张表的数据按规则拆分到同库的多张表中单表数据量过大
水平分库分表数据同时分到不同库的不同表中单表 + 单库都无法承受

分片策略

策略规则优点缺点
取模hash(sharding_key) % N数据分布均匀扩容需要迁移大量数据
范围按 ID 或时间范围扩容方便,只需新增分片可能数据倾斜(新数据集中在新分片)
一致性哈希环形哈希扩容只迁移部分数据实现复杂
查表法维护路由表灵活路由表本身可能成为瓶颈
取模分片示例
// 分 4 张表:orders_0, orders_1, orders_2, orders_3
int tableIndex = Math.abs(userId.hashCode()) % 4;
String tableName = "orders_" + tableIndex;
范围分片示例
// 按时间范围:2024 年的数据放 orders_2024
String tableName = "orders_" + year;

// 按 ID 范围:每 1000 万一张表
int tableIndex = (int)(id / 10000000);
String tableName = "orders_" + tableIndex;
分片键选择原则
  1. 选择查询频率最高的字段作为分片键(如按 user_id 分片,大部分查询都按 user_id)
  2. 数据分布应尽量均匀
  3. 避免跨分片查询(大部分查询应命中单个分片)

分库分表引入的问题

1. 分布式 ID

分库分表后不能使用数据库自增 ID(多个库独立自增会冲突)。

方案原理优点缺点
UUID128 位随机字符串简单,无中心化无序(影响 B+ 树插入)、长度大
Snowflake 雪花算法时间戳 + 机器 ID + 序列号有序、高性能依赖时钟,时钟回拨问题
数据库号段预分配号段,如 [1, 1000]简单,ID 连续依赖数据库
Redis INCRRedis 原子递增高性能依赖 Redis
Leaf(美团)号段 + Snowflake 双模式生产经验丰富需要部署服务
Snowflake 雪花算法 ID 结构(64 位)
// | 0 | 41位时间戳(毫秒) | 10位机器ID | 12位序列号 |
// | 1 | 41 bits | 10 bits | 12 bits |
//
// 41位时间戳:可用约 69 年
// 10位机器ID:最多 1024 个节点
// 12位序列号:每毫秒每节点 4096 个 ID

2. 跨分片查询

分片键查询可以路由到具体分片,但非分片键查询需要广播到所有分片

-- ✅ 命中分片(假设按 user_id 分片)
SELECT * FROM orders WHERE user_id = 1001;

-- ❌ 需要广播所有分片(无分片键)
SELECT * FROM orders WHERE order_no = 'ORD202401001';

-- ❌ 需要归并排序
SELECT * FROM orders ORDER BY create_time DESC LIMIT 10;

解决方案:

  • 冗余字段:常用查询条件冗余到分片键中
  • 全局表:不分片,所有库中保留完整副本(如字典表)
  • 绑定表:有关联关系的表使用相同的分片策略,保证 JOIN 在同一分片
  • 二级索引表:建立 order_no → user_id 的映射表

3. 跨分片事务

分库后无法使用单库事务,需要分布式事务

方案说明适用场景
XA 分布式事务两阶段提交,强一致对一致性要求极高
柔性事务(TCC)Try-Confirm-Cancel业务可拆分
最终一致性(Saga)补偿机制长事务
消息队列本地事务 + 消息确保最终一致异步场景

4. 跨分片排序和分页

查询 ORDER BY create_time LIMIT 10,需要从每个分片都查出 Top 10,再在内存中归并排序取最终 Top 10。

分页更复杂:LIMIT 100, 10 需要从每个分片查出前 110 条,归并后取第 101-110 条。

5. 扩容问题

取模分片扩容需要数据迁移。解决方案:

  • 提前规划:一次分够(如分 1024 张表),通过逻辑表到物理表的映射控制实际表数
  • 一致性哈希:扩容只迁移少量数据
  • 翻倍扩容:从 N 变 2N,只需迁移一半数据

常用中间件

中间件类型说明
ShardingSphere-JDBC客户端分片Java 端 jar 包,无需代理,性能好
ShardingSphere-Proxy代理层分片独立进程,对应用透明
MyCat代理层分片社区活跃度下降
Vitess代理层分片YouTube 开源,云原生
ShardingSphere-JDBC 分片配置示例
spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
url: jdbc:mysql://localhost:3306/db0
ds1:
url: jdbc:mysql://localhost:3306/db1
rules:
sharding:
tables:
orders:
actual-data-nodes: ds$->{0..1}.orders_$->{0..3}
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: orders-inline
key-generate-strategy:
column: id
key-generator-name: snowflake
sharding-algorithms:
orders-inline:
type: INLINE
props:
algorithm-expression: orders_$->{user_id % 4}
key-generators:
snowflake:
type: SNOWFLAKE

常见面试问题

Q1: 什么时候需要分库分表?

答案

一般来说,满足以下条件时考虑分库分表:

  • 单表数据量超过 2000 万行单表数据文件超过 20GB,查询性能明显下降
  • 数据库 QPS 超过单机上限(CPU、连接数、磁盘 I/O 瓶颈)
  • 已经做了 SQL 优化、索引优化、读写分离、缓存,仍无法满足需求

需要注意:分库分表是一个高成本操作,会引入分布式事务、跨分片查询、ID 生成等复杂问题,不应作为首选方案。

Q2: 垂直拆分和水平拆分的区别?

答案

维度垂直拆分水平拆分
拆分方式业务/字段拆分数据行拆分
解决问题表字段过多、业务解耦单表数据量过大
拆分后每个库/表的结构不同每个库/表的结构相同
复杂度相对较低较高(分布式 ID、跨分片查询等)

实际项目中通常先垂直后水平:先按业务模块拆库(微服务各自独立数据库),再对数据量大的表做水平分片。

Q3: 分片键如何选择?

答案

选择分片键的核心原则:

  1. 高频查询字段:大部分查询都应包含分片键,避免广播查询。例如 C 端业务以 user_id 为分片键,B 端以 merchant_id 为分片键
  2. 数据均匀分布:分片键的值应分布均匀,避免数据倾斜(某些分片数据远多于其他分片)
  3. 尽量避免跨分片操作:相关联的数据应在同一分片(如同一用户的订单和订单明细)

常见选择:用户 ID、订单 ID、租户 ID 等业务标识。

Q4: 分布式 ID 生成方案有哪些?

答案

主流方案对比:

方案有序性性能依赖适用场景
Snowflake趋势递增极高(本地生成)时钟大多数场景首选
号段模式连续递增数据库需要连续 ID
UUID无序高(本地生成)无序要求的场景
Redis INCR连续递增Redis简单场景

Snowflake 是最常用的方案:64 位 long 型,由时间戳(41 位)+ 机器 ID(10 位)+ 序列号(12 位)组成,每毫秒每机器可生成 4096 个 ID。需要注意时钟回拨的处理(可以等待或使用上次时间戳)。

Q5: 分库分表后如何做分页查询?

答案

这是分库分表最痛苦的问题之一。ORDER BY x LIMIT offset, size 需要:

  1. 向所有分片发送 LIMIT 0, offset+size 的查询
  2. 在内存中合并所有结果,重新排序
  3. 取出第 offset+1 到 offset+size 条数据

offset 越大,性能越差

优化方案:

  • 禁止深分页:限制最大页码,产品上引导用户缩小条件
  • 游标分页:记住上一页最后一条的排序值,下一页从该位置继续查(但不支持跳页)
  • 二次查询法:先查出 offset/N 附近的范围,再精确查询
  • 搜索引擎:将需要复杂查询的数据同步到 Elasticsearch

相关链接