MySQL 分库分表实战:垂直拆分、水平分片与 ShardingSphere 落地

主从复制和读写分离能扛住读流量,却扛不住写流量无限增长。当单库写性能、磁盘容量或连接数触及天花板时,分库分表就成了必经之路。本文从拆分策略讲到 ShardingSphere 落地,帮你把“拆”这件事拆明白。

一、单库撑不住时,先别急着分

分库分表是把双刃剑:它解决了容量与性能问题,却引入了复杂度。在决定拆分之前,先确认是否已经把单库优化到极致。

1.1 什么指标说明该拆了

瓶颈类型 常见阈值 说明
单表数据量 5000 万 ~ 1 亿行 InnoDB B+Tree 层级变深,范围查询变慢
单库数据量 500 GB ~ 1 TB 备份、恢复、迁移成本陡增
单库连接数 80% max_connections 应用池化再好也会被打满
单库写 TPS CPU / IO 长期 80%+ 索引维护、redo log、锁竞争放大

1.2 拆分前的三板斧

  1. SQL 与索引优化:慢查询、索引失效、大事务往往是真正元凶,详见 MySQL 索引优化专题。
  2. 读写分离:把读流量拆到从库,主库只负责写,成本比分片低得多。
  3. 缓存与归档:热数据放 Redis,冷数据归档,能大幅推迟拆分。

只有当“写”成为真正的瓶颈,或者单表/单库容量无法再扩展时,才值得引入分库分表。

二、垂直拆分:按业务拆库、冷热拆表

2.1 垂直分库

把不同业务模块的表放到独立的数据库实例中。

1
2
3
订单库:order、order_item、order_log
用户库:user、user_profile、user_address
商品库:product、category、inventory

优点

  • 各业务库独立扩容,互不拖累。
  • 降低单库连接数压力。

缺点

  • 跨库 JOIN 需要应用层聚合,或引入分布式查询中间件。
  • 分布式事务复杂度上升。

2.2 垂直分表

把宽表中冷热不均的字段拆出去。

原表 user 拆分后
id, nickname, phone, avatar, last_login 热表 user_basic
id, signature, address, tag_json, extend 冷表 user_extra

热表常驻内存缓存,冷表偶尔查询,能显著减少单表宽度与索引体积。

三、水平拆分:把同一张表的数据拆到多库

3.1 什么时候必须水平分

垂直拆分只能解决业务隔离问题,无法解决单表数据量过大。当一张表行数过亿、写入量持续增长,就需要水平拆分。

1
2
db_0: order_0, order_2, order_4
db_1: order_1, order_3, order_5

3.2 分片键(Sharding Key)是灵魂

分片键决定了数据落在哪个库/表。选错分片键,后患无穷。

维度 建议
访问频率 高频查询条件尽量命中同一分片
数据均匀 避免热点,例如按 user_id 比按 date 更均匀
业务语义 订单按 user_id,日志按时间,地理按 region_code
不可变 分片键尽量选不会修改的字段,否则数据迁移很痛苦

分片键一旦选定,后期迁移成本极高。上线前务必用真实数据模拟分布,检查是否有倾斜。

四、分片算法:怎么把数据路由到某个片

4.1 取模分片

1
2
shardingValue % dbCount  // 路由到库
tableValue % tableCount // 路由到表

优点:数据分布最均匀。
缺点:扩容时需要迁移大量数据(re-sharding)。

4.2 范围分片

按时间或 ID 范围切分:

1
order_202401, order_202402, order_202403...

优点:扩容只需新增分片,无需迁移历史数据。
缺点:新分片容易成为写热点,查询旧数据需跨片。

4.3 一致性哈希

在取模基础上引入虚拟节点,扩容时只影响相邻节点,迁移量降到最低。

1
2
虚拟节点 -> 物理节点
hash(user_id) -> 虚拟节点环 -> 物理库

适合节点数会动态变化的场景,但实现复杂度高于取模。

4.4 基因法解决“按非分片键查询”

订单表按 user_id 分片,但业务又常按 order_id 查询。可以在 order_id 中“嵌入” user_id 的基因片段:

1
2
order_id = 时间戳 + 基因(user_id) + 序列号
gene = user_id % 16 // 把分片信息写进 order_id

由 order_id 反解分片基因,即可直接路由到对应分片。

五、分布式 ID:分片后不能用自增了

5.1 自增主键的问题

多库各自自增会出现重复主键,合并查询时直接冲突。必须引入全局 ID 生成。

5.2 雪花算法

Twitter Snowflake 的核心结构:

1
0 | 41 bit 时间戳 | 10 bit 机器 ID | 12 bit 序列号

优点:趋势递增、毫秒级生成百万级 ID。
缺点:依赖机器时钟,时钟回拨会生成重复 ID。

生产改进:

  • 用 NTP + 闰秒处理避免回拨。
  • 改写为号段模式,批量缓存 ID 减少 RPC。

5.3 号段模式(Leaf)

美团 Leaf 的号段模式:

1
2
DB 表记录 biz_tag、max_id、step。
应用每次从 DB 取一段 ID 区间,本地用完再取。

优点:DB 压力小,生成速度快,可水平扩展。
缺点:号段用完后有短暂抖动,需要监控预警。

六、ShardingSphere 落地实战

Apache ShardingSphere 提供了两种接入方式:

模式 接入方式 优点 缺点
ShardingSphere-JDBC Java 依赖 性能高、无额外网络跳转 对业务代码有侵入
ShardingSphere-Proxy 独立代理 多语言通用、可集中管理 多一跳网络,运维成本高

6.1 分库分表配置示例(ShardingSphere-JDBC YAML)

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
dataSources:
ds_0: !!com.zaxxer.hikari.HikariDataSource
jdbcUrl: jdbc:mysql://127.0.0.1:3306/db_0
username: root
password: root
ds_1: !!com.zaxxer.hikari.HikariDataSource
jdbcUrl: jdbc:mysql://127.0.0.1:3306/db_1
username: root
password: root

rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..3}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: t_order_inline
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: database_inline
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake

shardingAlgorithms:
database_inline:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
t_order_inline:
type: INLINE
props:
algorithm-expression: t_order_${user_id % 4}

keyGenerators:
snowflake:
type: SNOWFLAKE

6.2 绑定表与广播表

  • 绑定表:具有相同分片规则的关联表,例如 t_ordert_order_item 都按 user_id 分片,JOIN 可下推到单库执行。
  • 广播表:数据量小、变化少的表(如字典表)同步到每个分片,避免跨库 JOIN。
1
2
3
4
bindingTables:
- t_order,t_order_item
broadcastTables:
- t_config

6.3 读写分离 + 分片组合配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rules:
- !READWRITE_SPLITTING
dataSources:
ds_0:
type: Static
props:
write-data-source-name: ds_0_master
read-data-source-names: ds_0_slave
ds_1:
type: Static
props:
write-data-source-name: ds_1_master
read-data-source-names: ds_1_slave

- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..3}

这样写流量路由到主库,读流量自动走从库,同时再做分片。

七、分库分表后的工程难题

7.1 跨片查询与分页

按 user_id 分片后,SELECT * FROM t_order ORDER BY create_time LIMIT 100 需要:

  1. 向所有分片发送同一条 SQL。
  2. 在 Proxy 或应用内存中归并排序。
  3. 取前 100 条。

深分页(OFFSET 很大)在分片环境下会被放大几十倍,性能灾难。应改用“上一页最大 ID + 时间戳”的游标分页。

7.2 跨片 JOIN

ShardingSphere 只能处理同一分片内的 JOIN。跨片 JOIN 需要:

  • 用绑定表把关联数据放在一起。
  • 在应用层拆两次查询再组装。
  • 引入宽表/ES/ClickHouse 做离线聚合。

7.3 分布式事务

分片后本地事务无法保证跨库一致性,常用方案:

方案 一致性 性能 适用场景
XA 两阶段提交 强一致 金融核心交易
BASE 柔性事务(Seata TCC / Saga) 最终一致 电商、物流
业务层幂等 + 对账 最终一致 最高 允许短时不一致

不要为了用分布式事务而用分布式事务。很多场景下,先把分片键选得足够好、让事务尽量落在一个分片内,才是更优解。

7.4 扩容与数据迁移

取模分片扩容时,需要把旧数据按新规则重新分布。常见做法:

  1. 双倍扩容:4 片扩到 8 片,每个旧片拆成两个新片,迁移量可控。
  2. 一致性哈希扩容:只迁移相邻节点的数据。
  3. 双写 + 增量同步:新库追平旧库后切流,保证可用性。

迁移期间一般采用:

  • 旧库继续写读。
  • 新库双写增量数据。
  • 全量比对一致后切读,再切写。

八、生产避坑清单

后果 建议
分片键选择只看均匀性 高频查询跨片 同时满足均匀 + 高频查询条件
全局自增 ID 主键冲突 使用雪花 / 号段 / UUID
深分页跨片归并 内存/CPU 爆炸 用游标分页,避免大 OFFSET
跨片 JOIN 在应用层硬拼 性能差、复杂度高 用绑定表、宽表或搜索引擎
忽略分片键不可变 数据需要迁移 选创建时间、用户 ID 等稳定字段
一次性拆太多片 运维复杂、资源浪费 先拆少,观察后再扩
没有灰度切换 全量故障 按用户维度灰度,先读再写

九、总结

分库分表是 MySQL 横向扩展的最后一道防线,不是性能优化的银弹:

  • 垂直拆分解决业务隔离与冷热分离,适合按模块拆库、按字段拆表。
  • 水平拆分解决单表容量与写瓶颈,核心是分片键分片算法
  • 取模均匀但扩容痛苦,范围扩容简单但易热点,一致性哈希在两者之间平衡。
  • 分布式 ID是自增主键的替代,雪花算法和号段模式最常用。
  • ShardingSphere 能把分片、读写分离、分布式事务以配置化方式落地,降低侵入。
  • 真正的难点不在“拆”,而在拆后的分页、JOIN、事务、迁移运维

如果你已经完成了索引优化、主从复制和读写分离,分库分表就是下一步让 MySQL 继续承载增长的合理选择。前提是:把分片键想清楚,把灰度方案做扎实。