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

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 拆分前的三板斧
- SQL 与索引优化:慢查询、索引失效、大事务往往是真正元凶,详见 MySQL 索引优化专题。
- 读写分离:把读流量拆到从库,主库只负责写,成本比分片低得多。
- 缓存与归档:热数据放 Redis,冷数据归档,能大幅推迟拆分。
二、垂直拆分:按业务拆库、冷热拆表
2.1 垂直分库
把不同业务模块的表放到独立的数据库实例中。
1 | 订单库:order、order_item、order_log |
优点:
- 各业务库独立扩容,互不拖累。
- 降低单库连接数压力。
缺点:
- 跨库 JOIN 需要应用层聚合,或引入分布式查询中间件。
- 分布式事务复杂度上升。
2.2 垂直分表
把宽表中冷热不均的字段拆出去。
| 原表 user | 拆分后 |
|---|---|
| id, nickname, phone, avatar, last_login | 热表 user_basic |
| id, signature, address, tag_json, extend | 冷表 user_extra |
热表常驻内存缓存,冷表偶尔查询,能显著减少单表宽度与索引体积。
三、水平拆分:把同一张表的数据拆到多库
3.1 什么时候必须水平分
垂直拆分只能解决业务隔离问题,无法解决单表数据量过大。当一张表行数过亿、写入量持续增长,就需要水平拆分。
1 | db_0: order_0, order_2, order_4 |
3.2 分片键(Sharding Key)是灵魂
分片键决定了数据落在哪个库/表。选错分片键,后患无穷。
| 维度 | 建议 |
|---|---|
| 访问频率 | 高频查询条件尽量命中同一分片 |
| 数据均匀 | 避免热点,例如按 user_id 比按 date 更均匀 |
| 业务语义 | 订单按 user_id,日志按时间,地理按 region_code |
| 不可变 | 分片键尽量选不会修改的字段,否则数据迁移很痛苦 |
分片键一旦选定,后期迁移成本极高。上线前务必用真实数据模拟分布,检查是否有倾斜。
四、分片算法:怎么把数据路由到某个片
4.1 取模分片
1 | shardingValue % dbCount // 路由到库 |
优点:数据分布最均匀。
缺点:扩容时需要迁移大量数据(re-sharding)。
4.2 范围分片
按时间或 ID 范围切分:
1 | order_202401, order_202402, order_202403... |
优点:扩容只需新增分片,无需迁移历史数据。
缺点:新分片容易成为写热点,查询旧数据需跨片。
4.3 一致性哈希
在取模基础上引入虚拟节点,扩容时只影响相邻节点,迁移量降到最低。
1 | 虚拟节点 -> 物理节点 |
适合节点数会动态变化的场景,但实现复杂度高于取模。
4.4 基因法解决“按非分片键查询”
订单表按 user_id 分片,但业务又常按 order_id 查询。可以在 order_id 中“嵌入” user_id 的基因片段:
1 | order_id = 时间戳 + 基因(user_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 | DB 表记录 biz_tag、max_id、step。 |
优点:DB 压力小,生成速度快,可水平扩展。
缺点:号段用完后有短暂抖动,需要监控预警。
六、ShardingSphere 落地实战
Apache ShardingSphere 提供了两种接入方式:
| 模式 | 接入方式 | 优点 | 缺点 |
|---|---|---|---|
| ShardingSphere-JDBC | Java 依赖 | 性能高、无额外网络跳转 | 对业务代码有侵入 |
| ShardingSphere-Proxy | 独立代理 | 多语言通用、可集中管理 | 多一跳网络,运维成本高 |
6.1 分库分表配置示例(ShardingSphere-JDBC YAML)
1 | dataSources: |
6.2 绑定表与广播表
- 绑定表:具有相同分片规则的关联表,例如
t_order与t_order_item都按user_id分片,JOIN 可下推到单库执行。 - 广播表:数据量小、变化少的表(如字典表)同步到每个分片,避免跨库 JOIN。
1 | bindingTables: |
6.3 读写分离 + 分片组合配置
1 | rules: |
这样写流量路由到主库,读流量自动走从库,同时再做分片。
七、分库分表后的工程难题
7.1 跨片查询与分页
按 user_id 分片后,SELECT * FROM t_order ORDER BY create_time LIMIT 100 需要:
- 向所有分片发送同一条 SQL。
- 在 Proxy 或应用内存中归并排序。
- 取前 100 条。
深分页(OFFSET 很大)在分片环境下会被放大几十倍,性能灾难。应改用“上一页最大 ID + 时间戳”的游标分页。
7.2 跨片 JOIN
ShardingSphere 只能处理同一分片内的 JOIN。跨片 JOIN 需要:
- 用绑定表把关联数据放在一起。
- 在应用层拆两次查询再组装。
- 引入宽表/ES/ClickHouse 做离线聚合。
7.3 分布式事务
分片后本地事务无法保证跨库一致性,常用方案:
| 方案 | 一致性 | 性能 | 适用场景 |
|---|---|---|---|
| XA 两阶段提交 | 强一致 | 低 | 金融核心交易 |
| BASE 柔性事务(Seata TCC / Saga) | 最终一致 | 高 | 电商、物流 |
| 业务层幂等 + 对账 | 最终一致 | 最高 | 允许短时不一致 |
7.4 扩容与数据迁移
取模分片扩容时,需要把旧数据按新规则重新分布。常见做法:
- 双倍扩容:4 片扩到 8 片,每个旧片拆成两个新片,迁移量可控。
- 一致性哈希扩容:只迁移相邻节点的数据。
- 双写 + 增量同步:新库追平旧库后切流,保证可用性。
迁移期间一般采用:
- 旧库继续写读。
- 新库双写增量数据。
- 全量比对一致后切读,再切写。
八、生产避坑清单
| 坑 | 后果 | 建议 |
|---|---|---|
| 分片键选择只看均匀性 | 高频查询跨片 | 同时满足均匀 + 高频查询条件 |
| 全局自增 ID | 主键冲突 | 使用雪花 / 号段 / UUID |
| 深分页跨片归并 | 内存/CPU 爆炸 | 用游标分页,避免大 OFFSET |
| 跨片 JOIN 在应用层硬拼 | 性能差、复杂度高 | 用绑定表、宽表或搜索引擎 |
| 忽略分片键不可变 | 数据需要迁移 | 选创建时间、用户 ID 等稳定字段 |
| 一次性拆太多片 | 运维复杂、资源浪费 | 先拆少,观察后再扩 |
| 没有灰度切换 | 全量故障 | 按用户维度灰度,先读再写 |
九、总结
分库分表是 MySQL 横向扩展的最后一道防线,不是性能优化的银弹:
- 垂直拆分解决业务隔离与冷热分离,适合按模块拆库、按字段拆表。
- 水平拆分解决单表容量与写瓶颈,核心是分片键和分片算法。
- 取模均匀但扩容痛苦,范围扩容简单但易热点,一致性哈希在两者之间平衡。
- 分布式 ID是自增主键的替代,雪花算法和号段模式最常用。
- ShardingSphere 能把分片、读写分离、分布式事务以配置化方式落地,降低侵入。
- 真正的难点不在“拆”,而在拆后的分页、JOIN、事务、迁移与运维。
如果你已经完成了索引优化、主从复制和读写分离,分库分表就是下一步让 MySQL 继续承载增长的合理选择。前提是:把分片键想清楚,把灰度方案做扎实。











