分库分表设计
问题
什么时候需要分库分表?水平拆分和垂直拆分有什么区别?常用的分片策略有哪些?
答案
为什么需要分库分表
| 瓶颈类型 | 表现 | 解决方案 |
|---|---|---|
| 单表数据量大 | 查询变慢(>2000 万行) | 水平分表 |
| 单库连接数不够 | Too many connections | 分库 |
| 单库磁盘不够 | 磁盘满 | 分库 |
| 单表列太多 | 行过宽,IO 大 | 垂直分表 |
分库分表是最后手段
优先考虑:SQL 优化 → 索引优化 → 读写分离 → 缓存 → 最后才分库分表。
垂直拆分
垂直分库
按业务模块拆分到不同数据库:
垂直分表
将一张宽表按列拆分为多张表:
-- 原始表(列太多,行太宽)
CREATE TABLE users (
id BIGINT, name VARCHAR(50), email VARCHAR(100),
avatar TEXT, bio TEXT, settings JSON,
-- ... 几十个字段
);
-- 垂直拆分
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100)
);
CREATE TABLE user_profiles (
user_id BIGINT PRIMARY KEY,
avatar TEXT,
bio TEXT,
settings JSON
);
原则:将常用字段和不常用字段(或大字段)分开存储。
水平拆分
将一张大表的数据按行拆分到多张结构相同的表中:
分片策略
1. 取模分片
分片编号 = sharding_key % 分片数量
-- user_id % 4 → 分到 orders_0, orders_1, orders_2, orders_3
-- user_id = 100 → 100 % 4 = 0 → orders_0
-- user_id = 101 → 101 % 4 = 1 → orders_1
| 优势 | 劣势 |
|---|---|
| 数据均匀分布 | 扩容需要数据迁移 |
| 实现简单 | 分片数量一般是 2 的幂 |
2. 范围分片
-- 按 ID 范围分片
-- orders_0: id 1 ~ 10,000,000
-- orders_1: id 10,000,001 ~ 20,000,000
-- ...
-- 按时间范围分片
-- orders_2024Q1: 2024-01 ~ 2024-03
-- orders_2024Q2: 2024-04 ~ 2024-06
| 优势 | 劣势 |
|---|---|
| 扩容方便(加新范围) | 可能数据不均匀(热点) |
| 范围查询友好 | 新数据集中在最后一个分片 |
3. 一致性 Hash
| 优势 | 劣势 |
|---|---|
| 扩容只迁移少量数据 | 实现相对复杂 |
| 动态增减节点 | 需要虚拟节点防止倾斜 |
分片键选择
分片键(Sharding Key)的选择至关重要:
| 原则 | 说明 |
|---|---|
| 高频查询字段 | 大部分查询都带这个条件 |
| 数据均匀分布 | 避免数据倾斜(热点) |
| 避免跨分片查询 | 尽量让一个请求只查一个分片 |
电商订单表常见分片键选择:
- user_id:用户维度查询为主(查我的订单)✅ 推荐
- order_id:适合按订单号精确查询
- create_time:适合按时间范围查询
分库分表带来的问题
| 问题 | 说明 | 解决方案 |
|---|---|---|
| 跨分片查询 | JOIN、聚合需跨多个分片 | 冗余数据、在应用层组装 |
| 跨分片排序 | ORDER BY 需要归并排序 | 中间件支持、限制查询范围 |
| 分布式事务 | 跨库无法用本地事务 | 分布式事务方案 |
| 全局唯一 ID | 自增 ID 冲突 | 分布式 ID 生成 |
| 扩容迁移 | 增加分片需要数据迁移 | 预分片、一致性 Hash |
| 运维复杂度 | 多个库/表管理 | ShardingSphere 等中间件 |
ShardingSphere
Apache ShardingSphere 是最流行的分库分表中间件:
| 产品 | 类型 | 说明 |
|---|---|---|
| ShardingSphere-JDBC | 客户端 jar | 轻量级,代码侵入小 |
| ShardingSphere-Proxy | 独立代理 | 对应用完全透明 |
# ShardingSphere 分片规则配置示例
rules:
- !SHARDING
tables:
orders:
actualDataNodes: ds_${0..1}.orders_${0..3}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: mod
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: db_mod
shardingAlgorithms:
mod:
type: MOD
props:
sharding-count: 4
常见面试问题
Q1: 什么时候需要分库分表?
答案:
单表数据量超过 2000 万行或单表大小超过 10GB 时,查询性能会明显下降。但这不是绝对标准,实际取决于:
- 查询复杂度(简单主键查 vs 复杂 JOIN)
- 索引设计是否合理
- 硬件配置(SSD vs HDD)
优先级:索引优化 → 读写分离 → 缓存 → 垂直拆分 → 水平分库分表。
Q2: 水平分表后,如何处理分页查询?
答案:
假设 4 个分表,查询第 2 页(每页 10 条,按时间排序):
- 全局排序归并:每个分片都查
LIMIT 20,应用层归并排序取第 11-20 条 - 问题:offset 越大越慢(每个分片都要查 offset + size 条)
- 优化:禁止深分页,用游标分页(
WHERE id > last_id LIMIT 10)
Q3: 分片键选了 user_id,按 order_id 查怎么办?
答案:
- 在 order_id 中编码分片信息:如 order_id 的最后 2 位表示分片号
- 建立映射表:
order_id → user_id的映射表(可放 Redis) - 全分片广播:向所有分片发送查询(性能最差,兜底方案)
- 数据冗余:按 order_id 再建一套分表(空间换时间)
Q4: 16 个库 × 64 个表够用吗?
答案:
16 库 × 64 表 = 1024 个物理表:
- 每个表 500 万行 → 总共支撑 50 亿行
- 每个表 1000 万行 → 总共支撑 100 亿行
对于大部分互联网业务足够。关键是提前规划好分片数量,因为后续扩容成本很高。