分库分表
→ 返回 数据库基础
当单表数据量超过千万、单库连接数和 IO 达到瓶颈时,分库分表是常见的扩展方案。核心原则:能不拆就不拆,先做索引优化、缓存、读写分离,最后才考虑分库分表。
什么时候需要分库分表
| 问题 | 阈值参考 | 解决方向 |
|---|---|---|
| 单表数据量过大,查询慢 | MySQL 单表 > 2000 万行(InnoDB + 合理索引) | 水平分表 |
| 单库连接数达上限 | MySQL max_connections 通常 1000~4000 | 分库 |
| 单库写入 TPS 过高 | 单机 InnoDB 写入约 1000~5000 TPS | 分库 |
| 表字段过多,数据冷热分离 | 单表 > 60 列,部分字段极少访问 | 垂直分表 |
| 不同业务模块在同一库互相影响 | — | 垂直分库 |
垂直拆分
垂直分库
按业务模块拆分:订单库、用户库、商品库各自独立部署:
单体数据库
├── user 表 → 用户库(user_db)
├── order 表 → 订单库(order_db)
├── product 表 → 商品库(product_db)
└── payment 表 → 支付库(pay_db)
优点:各模块独立扩展、故障隔离
缺点:跨库 JOIN 消失,需应用层聚合;分布式事务复杂度上升
垂直分表
将宽表按字段冷热拆分:常用字段 + 大字段分离:
-- 原始表(60+ 列)
users(id, name, email, password, avatar, bio, settings_json, ...)
-- 拆分后
users(id, name, email, password, status) -- 热数据,频繁查询
users_profile(user_id, avatar, bio, settings_json) -- 冷数据,按需加载水平拆分
将同一张表的数据按规则分散到多个物理表(分片):
order 表(10亿行)
├── order_0(0~2.5亿行)
├── order_1(2.5~5亿行)
├── order_2(5~7.5亿行)
└── order_3(7.5~10亿行)
分片键(Sharding Key)选择
分片键决定每行数据去哪个分片,选择不当后患无穷:
| 原则 | 说明 |
|---|---|
| 选择基数大的列 | 保证数据均匀分布(如 user_id,不选 status) |
| 选择查询高频列 | 查询时带分片键才能精确路由,否则散查所有分片 |
| 避免热点 | 若按时间分片,新数据集中在最新分片,产生写热点 |
| 业务含义清晰 | 订单表用 user_id 或 order_id,确保同一用户的订单在同一分片 |
分片策略
Hash 取模
shard_id = user_id % shard_count
user_id=100 → 100 % 4 = 0 → order_0
user_id=101 → 101 % 4 = 1 → order_1
优点:数据分布均匀
缺点:扩容时几乎所有数据需要迁移(user_id % 4 → user_id % 8)
范围分片
order_id 1~10000000 → order_0
order_id 10000001~20000000 → order_1
优点:扩容容易(新增分片即可)
缺点:写入热点(新数据都在最新分片)
一致性哈希
将所有分片节点分布在哈希环上,数据按 key 的哈希值顺时针找最近节点:
0
/ \
N3 N0
| |
N2 N1
\ /
180
优点:扩容只需迁移相邻分片的少量数据(1/n)
缺点:实现复杂,需虚拟节点防止数据倾斜
分库 + 分表(常见组合)
16 个分库 × 每库 32 张分表 = 512 张分表
路由公式:
db_index = user_id % 16
table_index = (user_id / 16) % 32
跨分片问题
分库分表带来一系列跨分片操作的复杂性:
跨分片查询 / JOIN
-- 原来:单库 JOIN
SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status='paid';
-- 分库后:users 和 orders 在不同库,无法直接 JOIN
-- 解决方案:
-- 1. 冗余字段(将 user_name 冗余到 orders 表中)
-- 2. 应用层聚合(先查 users 再查 orders,内存中 JOIN)
-- 3. 全局表(维度小的表每个分库都复制一份,如地区、分类)跨分片分页 / 排序
-- 分库后想按 created_at 排序取第 2 页
-- 必须查每个分片各取前 N 条,再在内存合并排序取第 2 页的 pageSize 条
-- → 越往后翻页,性能越差
-- 推荐方案:业务层避免深分页;或用游标 + 分片键保证查询只打一个分片跨分片聚合
-- COUNT / SUM / AVG 需要每个分片各自计算后在应用层合并
-- AVG 不能简单相加(需要 SUM 和 COUNT 分别汇总再计算)分布式事务
跨分片写入(如转账:A 分片扣款 + B 分片入账)需要分布式事务:
→ 参考 数据库事务 中的 TCC / SAGA 模式
全局唯一 ID
分库分表后,单库自增主键会重复,需要全局唯一 ID:
雪花算法(Snowflake)
64位 = 1位符号 + 41位时间戳(ms) + 10位机器ID + 12位序列号
- 单机每毫秒可生成 4096 个 ID
- 趋势递增,适合做分库分表的主键(B+ 树写入友好)
- 时钟回拨问题:时间戳依赖系统时钟,时钟回调会生成重复 ID
// 常用实现:Hutool IdUtil
long id = IdUtil.getSnowflakeNextId();
// 或自定义(传入 datacenterId 和 workerId)
Snowflake snowflake = IdUtil.createSnowflake(1, 1);
long id = snowflake.nextId();号段模式(Leaf-Segment)
从数据库批量取一段 ID(如每次取 1000 个),缓存在本地内存:
-- ID 分配表
CREATE TABLE id_alloc (
biz_tag VARCHAR(128) NOT NULL,
max_id BIGINT NOT NULL, -- 当前已分配到的最大 ID
step INT NOT NULL, -- 每次取的步长
PRIMARY KEY (biz_tag)
);
-- 取号(CAS 更新)
UPDATE id_alloc SET max_id = max_id + step WHERE biz_tag = 'order';
SELECT max_id FROM id_alloc WHERE biz_tag = 'order';
-- 本地可用范围:[max_id - step + 1, max_id]优点:ID 完全本地生成(号段用完前),性能高,不依赖时钟
实现:美团 Leaf、百度 UidGenerator
UUID
128 位随机字符串,全局唯一,但:
- 无序,写入 InnoDB 聚簇索引时频繁页分裂,性能差
- 存储占用大(36 字符 vs 8 字节 BIGINT)
- 不推荐作为数据库主键,可用于幂等键
分库分表中间件
| 中间件 | 形态 | 特点 |
|---|---|---|
| ShardingSphere-JDBC | 客户端 JAR | 代码内嵌,无额外网络跳转,性能好 |
| ShardingSphere-Proxy | 独立代理进程,兼容 MySQL 协议 | 应用无感知,支持异构语言 |
| MyCat | 独立代理 | 国内较早的分库分表中间件,功能全但社区活跃度下降 |
| Vitess | 分布式 MySQL 集群 | YouTube 开源,K8s 友好,生产级 |
ShardingSphere-JDBC 配置示例
# application.yml
spring:
shardingsphere:
datasource:
names: ds0, ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://db0:3306/order_db
ds1:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://db1:3306/order_db
rules:
sharding:
tables:
orders:
actual-data-nodes: ds$->{0..1}.orders_$->{0..3} # ds0.orders_0 ~ ds1.orders_3
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: db-inline
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: table-inline
sharding-algorithms:
db-inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2}
table-inline:
type: INLINE
props:
algorithm-expression: orders_$->{order_id % 4}
key-generators:
snowflake:
type: SNOWFLAKE迁移方案(存量数据迁移)
1. 双写阶段
新数据同时写入老库(单表)和新库(分片)
2. 历史数据迁移
DataX / 自研脚本将老库数据按分片键路由写入新库
3. 数据校验
对比新旧库行数、checksum,确保一致
4. 切流
读请求逐步切到新库(灰度放量)
全量切流后,老库保留一段时间作为回滚方案
5. 停止双写,下线老库