字数:约4200字 | 阅读时间:15分钟
“当单库撑不住的那一刻,不是系统的终点,而是架构升级的起点。”


数据是系统的血液,业务增长必然带来数据量的膨胀。一个创业项目初期,单库MySQL足以支撑;但当订单量破亿、用户过千万、日增数据以百万计的时候,数据库就成了整个系统最脆弱的瓶颈。

这不是危言耸听,这是2026年每个中大型系统的必经之路。

本文以一个订单系统为例,从容量评估讲起,覆盖垂直拆分、水平拆分、分片键选择、ShardingSphere实战、分布式ID生成、跨分片查询与分布式事务处理,带你完成从单库到万亿级数据的完整演进。

一、何时该考虑分库分表

1.1 容量评估的量化标准

不是所有业务都需要分库分表,过早拆分是徒增复杂度,过晚拆分是积重难返。以下是2026年业界公认的量化红线:

指标 单库上限 建议动作
单表数据量 >5000万条 考虑水平拆分
数据库容量 >500GB 考虑垂直拆分
QPS(读+写) >2万 考虑读写分离
单节点QPS >5万 必须分库
主从同步延迟 >100ms 架构审查

以订单系统为例,假设日增订单50万条,保留2年数据:

  • 数据量估算:50万 × 730天 ≈ 3.65亿条订单
  • 每条订单按1KB算:365GB原始数据 + 索引 ≈ 500GB+

这已经触发单表5000万和容量500GB的双重红线。

1.2 瓶颈判断的实战经验

除了量化指标,还要关注以下实操层面的信号:

慢查询常态化。 当EXPLAIN分析显示type=ALL(全表扫描)或Using filesort成为常态,即使数据量还没到红线,索引设计已经无法解决。

连接池频繁告警。 MySQL默认max_connections=151,单库并发连接接近这个值时,每次连接建立都在和阈值赛跑。监控面板上Threads_connected长期在100以上,就是分库信号。

备份窗口失效。 500GB数据用mysqldump备份需要2-3小时,这已经超出业务可接受的备份窗口(通常<1小时)。物理备份(xtrabackup)可以缓解,但不是根本解法。

一句话判断: 如果你发现加索引、加硬件、加缓存都只能短暂缓解,而不是根本解决问题,那就是分库分表的时机。

二、垂直拆分 vs 水平拆分

分库分表的两条路,适用场景截然不同。

2.1 垂直拆分:按业务拆分

垂直拆分是把一个库里的不同业务表拆到不同库或不同实例。

1
2
3
4
5
6
# 拆分前:单库包含所有业务表
orders -- 订单表
users -- 用户表
products -- 商品表
inventory -- 库存表
payments -- 支付表
1
2
3
4
# 拆分后:按业务域拆分
订单库:orders, payments
用户库:users, user_accounts
商品库:products, inventory

适用场景: 不同业务模块访问量差异大,某几个表成为热点而拖累其他表;或者不同表需要不同的数据库配置(如事务隔离级别、字符集)。

实操注意点: 垂直拆分后,原本的跨库JOIN(多表联查)必须通过应用层JOIN或数据冗余解决。这是架构师必须提前和业务方确认的。

2.2 水平拆分:按数据拆分

水平拆分是把同一张表的数据按某个维度拆分到多个库或多个表。

1
2
# 拆分前:单表数据量破亿
orders ——> 1亿条订单
1
2
3
4
5
# 拆分后:按用户ID取模拆分
orders_0 ——> 用户ID % 4 == 0 的订单(2500万)
orders_1 ——> 用户ID % 4 == 1 的订单(2500万)
orders_2 ——> 用户ID % 4 == 2 的订单(2500万)
orders_3 ——> 用户ID % 4 == 3 的订单(2500万)

适用场景: 单表数据量已经无法通过加索引解决,访问量持续增长,单库CPU/内存/IO成为瓶颈。

实操注意点: 水平拆分后,跨分片查询是最大的挑战。选择分片键时必须确保查询条件能命中分片键,否则全分片扫描是性能灾难。

2.3 何时用哪种

维度 垂直拆分 水平拆分
拆分依据 业务模块 数据特征
数据量 单表不大,但库大 单表数据量巨大
复杂度 中等
跨库查询 部分可避免 几乎不可避免
迁移成本
推荐时机 业务模块独立性强 单表成为瓶颈

实际生产中,通常是两者结合使用:先垂直拆分出独立的业务库,再对数据量最大的表做水平拆分。

三、分片键选择与数据迁移

3.1 分片键选择原则

分片键是数据拆分的核心依据,选错了代价极高——几乎所有跨分片查询问题都源于分片键选择不当。

核心原则一:查询频率最高的字段优先。

如果80%的查询都带user_id,那就按user_id分片。不要按order_id分片然后天天跨分片查”该用户的所有订单”。

核心原则二:避免数据倾斜。

如果按region分片,北上广深的用户占了80%,那这几个区的分片会先爆掉,其他分片却空着。分片键的取值分布要足够均匀。

核心原则三:分片键要稳定。

订单一旦分片,就不能改分片键。如果用户ID在特定情况下会变化(如用户体系合并),就要提前预留方案。

3.2 常见分片策略

哈希分片(Hash Sharding): 对分片键做哈希取模。分布均匀,但扩容时需要迁移数据(所有路由规则都要变)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- ShardingSphere配置示例
sharding:
tables:
orders:
actualDataNodes: ds_${0..3}.orders_${0..3}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: orders_hash
shardingAlgorithms:
orders_hash:
type: INLINE
props:
algorithm-expression: orders_${user_id.hashCode() % 4}

范围分片(Range Sharding): 按时间或ID范围分片。适合时序数据,但容易产生热点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 按月份分片示例
sharding:
tables:
orders:
actualDataNodes: ds_0.orders_${202601..202612}
tableStrategy:
standard:
shardingColumn: create_time
shardingAlgorithmName: orders_month
shardingAlgorithms:
orders_month:
type: INLINE
props:
algorithm-expression: orders_${create_time.substring(0, 7).replace("-", "")}

目录分片(Lookup Sharding): 维护一张分片键到数据节点的映射表。灵活但增加一次查表开销。

3.3 数据迁移方案

分库分表最难的不是配置,是数据迁移。推荐双写双读 + 灰度切换方案:

阶段一:双写(写入新旧两个数据源)

应用层在写入时同时写旧库和新分片表。新分片表初始为空,数据靠历史迁移。

阶段二:历史数据迁移

用ETL工具(如DataX、Kettle)把历史数据迁移到新分片。迁移过程中新旧数据并存,以新分片数据为准做对账,确保数据一致。

阶段三:灰度切换读流量

先切换10%流量读新分片,监控数据一致性和延迟。逐步提升灰度比例到100%。

阶段四:下线旧库

确认所有流量已切换到新分片,且无异常,保留旧库30天备份后下线。

迁移过程的对账是硬功夫。 建议每天跑一次数据一致性校验脚本,发现差异立刻告警并修复。迁移窗口期出问题没有回头路。

四、ShardingSphere实战

4.1 ShardingSphere是什么

ShardingSphere是2026年分库分表领域最成熟的中间件方案,Apache顶级项目,支持MySQL、PostgreSQL、SQLServer等多种数据库。

核心组件:

  • ShardingSphere-JDBC:轻量级,Java应用内嵌使用,无额外运维成本
  • ShardingSphere-Proxy:独立部署,支持多语言,支持MySQL和PostgreSQL协议

本文以ShardingSphere-JDBC 5.5.x(支持Java 21)为例。

4.2 核心配置实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# sharding sphere配置
schemaName: orders_db

dataSources:
ds_0:
url: jdbc:mysql://192.168.1.10:3306/orders?useSSL=false&serverTimezone=Asia/Shanghai
username: orders_user
password: xxxxxx
connectionPoolClassName: com.zaxxer.hikari.HikariDataSource
maximumPoolSize: 20
minimumIdle: 5
ds_1:
url: jdbc:mysql://192.168.1.11:3306/orders?useSSL=false&serverTimezone=Asia/Shanghai
username: orders_user
password: xxxxxx
connectionPoolClassName: com.zaxxer.hikari.HikariDataSource
maximumPoolSize: 20
minimumIdle: 5
ds_2:
url: jdbc:mysql://192.168.1.12:3306/orders?useSSL=false&serverTimezone=Asia/Shanghai
username: orders_user
password: xxxxxx
connectionPoolClassName: com.zaxxer.hikari.HikariDataSource
maximumPoolSize: 20
minimumIdle: 5
ds_3:
url: jdbc:mysql://192.168.1.13:3306/orders?useSSL=false&serverTimezone=Asia/Shanghai
username: orders_user
password: xxxxxx
connectionPoolClassName: com.zaxxer.hikari.HikariDataSource
maximumPoolSize: 20
minimumIdle: 5

rules:
- !SHARDING
tables:
orders:
actualDataNodes: ds_${0..3}.orders_${0..3}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: orders_inline
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake
bindingTables:
- orders,order_items
shardingAlgorithms:
orders_inline:
type: INLINE
props:
algorithm-expression: orders_${user_id.hashCode() % 4}
keyGenerators:
snowflake:
type: SNOWFLAKE
props:
ts-bits: 12
wo-bits: 5
seq-bits: 6

这个配置定义了4个数据源(ds_0 ~ ds_3),每个数据源4张表(orders_0 ~ orders_3),共16个分片,按user_id哈希取模分散到16个分片中。

4.3 读写分离配置

结合主从复制做读写分离:

1
2
3
4
5
6
7
8
9
rules:
- !READWRITE_SPLITTING
dataSources:
ds_0:
type: Static
props:
write-data-source-name: ds_0_master
read-data-source-names: ds_0_slave_0,ds_0_slave_1
load-balancer-type: ROUND_ROBIN

读请求分发到从库,写请求打到主库,减轻主库压力。

五、分布式ID生成:雪花算法与UUID

分库分表后,自增ID的局限性暴露无遗:不同分片的ID会重复。必须有全局唯一ID方案。

5.1 雪花算法(Snowflake)

Twitter开源的分布式ID算法,2026年依然是国内互联网的主流选择。

算法原理:64位二进制,分三段

1
2
3
4
+------+------+------+
| 符号 | 时间戳 | 序列号 |
+------+------+------+
1位 41位 22位
  • 第1位:固定0(正数)
  • 41位时间戳:相对2016-11-04的毫秒差,可用69年
  • 10位机器ID:5位机房+5位机器
  • 12位序列号:每节点每毫秒最多4096个ID

Java 21实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class SnowflakeIdGenerator {
private final long epoch = 1672531200000L; // 2023-01-01作为epoch
private final long timestampBits = 41L;
private final long workerIdBits = 10L;
private final long sequenceBits = 12L;

private final long maxWorkerId = ~(-1L << workerIdBits);
private final long maxSequence = ~(-1L << sequenceBits);

private final long workerIdShift = sequenceBits;
private final long timestampShift = sequenceBits + workerIdBits;

private final long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;

public SnowflakeIdGenerator(long workerId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("worker Id can't be greater than " + maxWorkerId);
}
this.workerId = workerId;
}

public synchronized long nextId() {
long timestamp = System.currentTimeMillis() - epoch;

if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id.");
}

if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & maxSequence;
if (sequence == 0) {
timestamp = waitNextMillis(timestamp);
}
} else {
sequence = 0L;
}

lastTimestamp = timestamp;

return (timestamp << timestampShift)
| (workerId << workerIdShift)
| sequence;
}

private long waitNextMillis(long timestamp) {
while (timestamp == lastTimestamp) {
timestamp = System.currentTimeMillis() - epoch;
}
return timestamp;
}
}

优点: 有序数值型,查询友好(主键索引友好),性能高
缺点: 依赖服务器时间,时间回拨会出问题

5.2 UUID

通用唯一标识符,128位,标准格式550e8400-e29b-41d4-a716-446655440000

Java 21实现:

1
2
3
4
5
6
7
8
9
10
11
// 标准UUID(无意义字符串)
String uuid = UUID.randomUUID().toString();

// 改进版:利用时间戳和随机数拼凑
public class TimeBasedUUID {
public static String generate() {
long time = System.currentTimeMillis();
long random = ThreadLocalRandom.current().nextLong();
return new UUID(time, random).toString();
}
}

优点: 不依赖机器时钟,本地生成,无网络开销
缺点: 128位比64位长,存储成本高;UUID字符串无序,主键索引插入性能差

5.3 选型建议

维度 雪花算法 UUID
ID长度 64位(19位十进制) 128位(36位字符串)
有序性 有序 无序
主键索引 友好 插入性能差
依赖 服务器时钟
时钟回拨 有隐患
国内主流 ❌(仅特定场景)

结论:订单、用户等业务ID用雪花算法;日志、追踪等无序ID用UUID。

六、跨分片查询与分布式事务

6.1 跨分片查询的解决思路

分库分表后,跨分片查询无法完全避免。以下是几种处理思路:

思路一:业务层JOIN

如果按user_id分片,”查用户订单”直接路由到对应分片,应用层做JOIN。如果按order_id分片,”查某订单的商品”就需要广播查询(所有分片执行同一SQL),性能很差。

所以分片键选错了,跨分片查询就是噩梦。

思路二:异构索引表(ES搜索)

将分片后的数据同步到ElasticSearch,用ES做复杂查询(分页、排序、全文检索),业务层从ES拿到ID列表后再从分片取数据。

1
2
3
// 典型架构
// 写入:订单数据 -> 分片数据库 + 同步到ES
// 查询:复杂查询 -> ES -> 分片数据库取明细

思路三:族谱化查询(Scatter-Gather)

将查询广播到所有分片,各分片返回本地结果后合并排序。这是最无奈的做法,通常用于BI报表,不适合高并发OLTP场景。

6.2 分布式事务的解决方案

跨分片的数据一致性是分库分表最复杂的问题。以下是2026年主流方案:

方案一:Seata AT模式

ShardingSphere + Seata是最成熟的组合。AT模式是二阶段提交的自动补偿模式,对业务代码零侵入。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- Seata配置 -->
<seata:registry>
<seata:config type="nacos">
<seata:config-file>registry.conf</seata:config-file>
</seata:config>
</seata:registry>

<!-- 业务代码(零侵入) -->
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order, List<OrderItem> items) {
orderMapper.insert(order); // 自动向TC注册分支事务
items.forEach(item -> itemMapper.insert(item)); // 自动补偿
}

方案二:Saga模式

长流程业务(如订单→支付→库存→物流)用Saga模式,每个子事务有正向和补偿操作。前端提交成功,任意一步失败后逆序执行补偿。

方案三:TCC(Try-Confirm-Cancel)模式

更细粒度的两阶段提交。Try阶段预留资源,Confirm阶段确认执行,Cancel阶段释放预留。对业务有一定侵入性,但性能好。

6.3 实际案例:一个订单的分布式事务

1
2
3
4
5
6
7
8
9
10
11
场景:创建订单,扣减库存,生成支付单

错误做法(先扣库存,再创订单,最后支付):
1. 库存服务超时 -> 订单已创(数据不一致)
2. 支付服务超时 -> 库存已扣但未支付(数据不一致)

正确做法(Seata AT模式):
1. TM开启全局事务(获取XID)
2. Try:订单服务创建订单(预检查),库存服务预扣(预留)
3. Confirm:所有子事务提交,库存真正扣减,支付单生成
4. Cancel:任意失败,自动补偿回滚所有子事务

七、扩容实战:从4分片到8分片

业务增长,分片不够用了怎么办?扩容是分库分表永恒的难题。

哈希分片的扩容代价是全量数据迁移。 4分片变8分片,所有数据的分片键哈希取模结果都变了。

7.1 扩容方案:一致性哈希

一致性哈希环减少数据迁移量:

1
2
3
4
5
6
分片4个节点时,数据按hash(key) % 4路由
分片扩容到8个节点时,变为hash(key) % 8

key=1: 1%4=1 -> 迁移到 1%8=1(不变)
key=5: 5%4=1 -> 迁移到 5%8=5(变了!)
key=9: 9%4=1 -> 迁移到 9%8=1(不变)

理论上哈希分片扩容需要迁移50%的数据,**一致性哈希环可以把迁移量控制在25-30%**。

7.2 扩容实战步骤

阶段一:双写双读

新加ds_4、ds_5、ds_6、ds_7四个节点,应用层配置同时写新旧节点,读全部节点做合并去重。

阶段二:历史数据迁移

用迁移工具将ds_0的历史数据按新路由规则迁移到新分片。迁移过程中新旧分片数据并存。

阶段三:灰度切换

先切换10%流量用新路由规则,逐步到100%。每次切换监控延迟和错误率。

阶段四:下线旧分片

确认无误后,保留旧分片备份30天,然后下线。

扩容窗口的数据一致性是最需要注意的。 建议用双写期间的新增数据对账脚本,逐条比对新老分片的数据差异。

八、总结

分库分表是系统架构中少有的”没有回头路”的设计决策。一旦拆分,改动的代价极高,所以在动手之前要充分评估:

  1. 容量红线到了没有? 不要过早拆分,也不要过晚拆分
  2. 分片键选对了吗? 这是最重要的单点决策,决定了后续80%的复杂度
  3. 迁移方案可行吗? 历史数据迁移是对账能力和耐心的双重考验
  4. 分布式事务方案确定了吗? 跨分片事务处理要提前设计和测试

最后记住一句话:分库分表是架构升级的手段,不是解决所有数据库问题的万灵丹。 在加索引、加硬件、加缓存、加读写分离能解决问题的情况下,不要轻易走这条路。

但当业务规模到了那个量级,勇敢迈出去,熬过迁移的阵痛期,就是一片新天地。


相关工具版本(2026年):

  • Java: 21(LTS)
  • ShardingSphere: 5.5.x
  • MySQL: 8.0 / PostgreSQL: 17
  • Seata: 2.0.x
  • Redis: 8.0