单表数据量到了千万级,查询开始变慢,索引也救不了。这时候就得考虑分库分表了。
但分库分表不是拍脑袋就能干的,分不好反而更慢。今天把核心思路讲清楚。
什么时候需要分
先明确一个前提:能用索引优化解决的,别急着分库分表。
| 单表数据量 | 建议 |
|---|---|
| < 500万 | 加索引、优化SQL |
| 500万 - 2000万 | 考虑归档历史数据、读写分离 |
| > 2000万 | 分库分表 |
这个数字不是绝对的,和字段数量、行大小、查询模式都有关系。但超过2000万行就该认真评估了。
垂直拆分
垂直分库:按业务拆,订单库、用户库、商品库分开。
单库
├── user表
├── order表
└── product表
拆成
├── 用户库:user表
├── 订单库:order表
└── 商品库:product表
垂直分表:把字段多的大表拆成主表和扩展表。
user表(20个字段)
↓
user_base(id, name, phone, status)
user_ext(id, avatar, bio, settings)
把不常查的大字段(text、blob)拆出去,主表更小,查询更快。
水平拆分
水平分表:同一个表的数据按规则分散到多张表。
order表(1亿行)
↓
order_0、order_1、order_2、order_3
水平分库:分到不同数据库实例,进一步分散压力。
分片键怎么选
这是分库分表最关键的一步,选错了后面很难改。
| 分片键 | 优点 | 缺点 |
|---|---|---|
| 用户ID | 查询天然带用户维度 | 按订单号查要走全库 |
| 订单ID | 按订单查快 | 查用户所有订单要扫全库 |
| 时间 | 便于归档 | 热点集中在新数据 |
| 哈希取模 | 数据均匀 | 扩容需要迁移 |
实际项目里常用基因法:订单号里嵌入用户ID的后几位,这样按订单号也能定位到分片。
常见分片算法
取模:
int shard = userId.hashCode() % 4;
// 简单,但扩容时几乎所有数据都要迁移
范围:
// 按ID区间分
if (id < 10000000) return "db0";
else if (id < 20000000) return "db1";
一致性哈希:扩容时只迁移部分数据,但实现复杂。
雪花算法+基因位:ID本身携带分片信息,最常用。
分库分表带来的问题
| 问题 | 说明 | 方案 |
|---|---|---|
| 跨库JOIN | 不能直接JOIN | 冗余字段、应用层组装 |
| 分布式事务 | 跨库写入要保证一致 | Seata、最终一致性 |
| 分页 | 跨库分页很麻烦 | 二次查询法、禁止深分页 |
| 全局ID | 自增ID会冲突 | 雪花算法、号段模式 |
| 聚合统计 | count/sum要汇总 | 汇总表、离线计算 |
什么时候用中间件
手写分片逻辑维护成本高,一般用中间件:
- ShardingSphere:生态好,支持分库分表、读写分离、数据加密
- MyCat:老牌代理式中间件
# ShardingSphere配置示例
spring:
shardingsphere:
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..3}
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: order-mod
实施顺序建议
- 先做读写分离,缓解读压力
- 再做垂直拆分,按业务解耦
- 最后做水平分表,解决单表过大
- 历史数据归档,优先于分表
别一上来就上分库分表,架构复杂度会陡增。能用简单方案解决的,就别给自己找麻烦。