Appearance
分库分表策略详解
一、什么是分库分表?
分库分表是数据库水平扩展的核心技术,通过将数据分散到多个数据库或数据表中,解决单库单表数据量过大、性能瓶颈的问题。
基本概念
| 概念 | 说明 | 示例 |
|---|---|---|
| 分库 | 将数据分散到多个数据库中 | 用户表按用户ID取模,分散到 user_db_0、user_db_1、user_db_2 |
| 分表 | 将数据分散到同一数据库的多个表中 | 订单表按月份拆分:order_202401、order_202402、order_202403 |
| 分库分表 | 同时进行分库和分表 | 先分库,每个库内再分表 |
二、为什么需要分库分表?
1. 单库单表的瓶颈
性能瓶颈
- 数据量过大:单表数据超过千万级别,查询性能急剧下降
- 索引效率降低:B+树索引深度增加,查询需要更多磁盘IO
- 锁竞争加剧:高并发下,表级锁或行级锁竞争激烈
- 连接数限制:单库连接数有限,无法支撑高并发
容量瓶颈
- 存储空间:单库存储容量有限
- 备份恢复:数据量过大,备份和恢复时间过长
- 运维困难:DDL操作(如添加索引)耗时过长,影响业务
2. 何时考虑分库分表?
判断标准:
- ✅ 单表数据量超过 500万-1000万 行
- ✅ 数据库QPS超过 5000-10000
- ✅ 单表文件大小超过 10GB
- ✅ 查询响应时间明显变慢(>1秒)
- ✅ 频繁出现慢查询
注意:不要过早优化!优先考虑:
- 优化SQL和索引
- 读写分离
- 缓存优化
- 数据库参数调优
三、分库分表策略
1. 水平分表(Horizontal Sharding)
将同一张表的数据按行拆分到多个表中,表结构相同。
策略分类
① 范围分片(Range Sharding)
原理:按数据范围划分
示例:
sql
-- 按用户ID范围分表
user_0: 0 <= user_id < 1000000
user_1: 1000000 <= user_id < 2000000
user_2: 2000000 <= user_id < 3000000
优点:
- 实现简单,易于理解
- 范围查询效率高
- 数据迁移方便
缺点:
- 容易产生数据倾斜(热点数据集中)
- 需要提前规划范围
- 扩容需要迁移数据
② 哈希分片(Hash Sharding)
原理:对分片键进行哈希运算,取模分配到不同表
示例:
python
# 按用户ID取模分表
shard_num = user_id % 4
table_name = f"user_{shard_num}"
# 用户ID=1001 -> 1001 % 4 = 1 -> user_1
# 用户ID=1002 -> 1002 % 4 = 2 -> user_2
优点:
- 数据分布均匀,避免热点
- 实现简单,查询路由明确
- 适合高并发场景
缺点:
- 扩容困难(需要重新哈希)
- 范围查询需要跨表
- 无法按业务逻辑分片
③ 一致性哈希(Consistent Hashing)
原理:将哈希值空间组织成虚拟环,节点和数据都映射到环上
示例:
哈希环: 0 ----1000----2000----3000----4000----0
节点: Node1 Node2 Node3 Node4
数据: key1->Node2, key2->Node3, key3->Node4
优点:
- 扩容时只需迁移少量数据
- 节点增减影响范围小
- 适合动态扩容场景
缺点:
- 实现复杂
- 可能出现数据倾斜(虚拟节点解决)
④ 目录分片(Directory Sharding)
原理:维护一个分片映射表,记录数据到分片的映射关系
示例:
sql
-- 分片映射表
CREATE TABLE shard_mapping (
user_id BIGINT PRIMARY KEY,
shard_id INT,
table_name VARCHAR(50)
);
-- 查询时先查映射表
SELECT shard_id, table_name FROM shard_mapping WHERE user_id = 1001;
优点:
- 灵活,支持复杂分片规则
- 易于调整分片策略
- 支持非均匀分片
缺点:
- 需要额外维护映射表
- 映射表可能成为瓶颈
- 查询需要两次访问
2. 垂直分表(Vertical Sharding)
将同一张表按列拆分,将不常用或大字段分离到扩展表。
示例:
sql
-- 主表:存储常用字段
CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100),
created_at TIMESTAMP
);
-- 扩展表:存储不常用字段
CREATE TABLE user_ext (
user_id BIGINT PRIMARY KEY,
bio TEXT,
avatar_url VARCHAR(255),
settings JSON
);
优点:
- 减少单表字段数,提升查询效率
- 大字段分离,减少IO
- 可按访问频率优化
缺点:
- 需要JOIN查询
- 事务处理复杂
- 数据一致性需要保证
3. 垂直分库
按业务模块将不同表拆分到不同数据库。
示例:
user_db: 用户相关表(user, user_profile, user_settings)
order_db: 订单相关表(order, order_item, payment)
product_db: 商品相关表(product, category, inventory)
优点:
- 业务解耦,便于独立扩展
- 降低单库压力
- 便于团队分工
缺点:
- 跨库事务复杂(需要分布式事务)
- 跨库JOIN困难
- 数据一致性保证困难
四、分片键(Sharding Key)选择
1. 分片键的重要性
分片键是决定数据分布的关键字段,选择不当会导致:
- 数据倾斜(热点问题)
- 跨分片查询增多
- 扩容困难
2. 选择原则
① 高基数(Cardinality)
分片键的值应该尽可能分散,避免集中在少数几个值。
好的选择:
- 用户ID(每个用户唯一)
- 订单ID(每个订单唯一)
- UUID(全局唯一)
不好的选择:
- 性别(只有2-3个值)
- 状态(只有几个状态值)
- 地区(值有限)
② 业务相关性
分片键应该与业务查询模式匹配,减少跨分片查询。
示例:
sql
-- 如果经常按用户ID查询订单
-- 分片键选择:user_id(而不是order_id)
-- 这样同一用户的订单在同一分片
SELECT * FROM order WHERE user_id = 1001;
③ 避免热点
避免选择会导致数据集中在少数分片的字段。
问题示例:
sql
-- 按创建时间分片,新数据都集中在最新分片
order_202412: 大量新订单(热点)
order_202411: 少量订单
order_202410: 更少订单
解决方案:
- 使用组合分片键:
(user_id, create_time) - 使用哈希分片而非范围分片
3. 常见分片键选择
| 业务场景 | 推荐分片键 | 原因 |
|---|---|---|
| 用户表 | user_id | 高基数,查询模式匹配 |
| 订单表 | user_id | 按用户查询多,避免跨分片 |
| 订单表 | order_id | 如果按订单ID查询多 |
| 商品表 | category_id + product_id | 组合键,按分类查询 |
| 日志表 | user_id + date | 组合键,按用户和时间查询 |