分库分表方案:ShardingSphere vs MyCat 对比
一、为什么需要分库分表?
单表数据量达到亿级时,MySQL 会遇到三个瓶颈:
- IO 瓶颈:单张表的数据文件过大,B+ 树高度从 3 层升到 4-5 层(16KB page,假设每页存储约 1000 条索引记录,单表 5000 万行时索引树高度约 3 层,到 5 亿行时升到 4 层,每次查询多一次磁盘 IO),每次查询需要更多 IO 次数
- 写入瓶颈:单库的写入吞吐受限于磁盘 IOPS,一台 8 核 32G 的 ECS 上 MySQL 单库写入 TPS 通常在 1~2 万(纯顺序写 redo log 时可达 10 万,但加上随机写数据页和 binlog 同步后降到 1 万出头),无法支撑高并发写入
- 连接瓶颈:单库默认连接数 151(max_connections 默认值),即使调高到 2000,连接池在应用层也会因线程争抢而大幅降速
分库分表不是银弹,但它是数据量突破单机极限时不得不走的路。
二、分库分表的核心策略
1. 垂直分库
按业务模块拆分:用户库、订单库、商品库。本质上不是技术问题,是业务架构的解耦。微服务化之后,数据库自然跟着拆分。
2. 水平分表
按某个分片键(Sharding Key)将数据均匀分布到多个表/库中。常见算法:
| 算法 | 特点 | 典型场景 | 扩容难度 | 数据倾斜风险 |
|---|---|---|---|---|
| 取模(Hash Mod) | 数据分布均匀,精确查找快 | 用户 ID 取模 | 困难(需要全量迁移) | 低(ID 均匀时) |
| 范围分片(Range) | 数据连续,范围查询友好 | 按时间分片 | 容易(只需加新库) | 高(热点集中在最新分片) |
| 一致性哈希 | 扩容仅迁移少量数据 | 缓存分片 | 容易 | 中(需虚拟节点调节) |
三、ShardingSphere vs MyCat 核心对比
架构对比
ShardingSphere(Apache):无中心化架构,包含两种模式:
- Sharding-JDBC:JDBC 层客户端分片,直接嵌入应用,应用启动时加载分片规则。SQL 流程:应用 → Sharding-JDBC 解析 SQL → 路由计算 → 重写 SQL → 执行到各分片 → 结果归并。整个过程在应用进程内完成,无网络跳转。
- Sharding-Proxy:代理层分片,部署在应用和数据库之间,应用无感知。兼容 MySQL 协议,可以直连 MySQL 客户端、Navicat 等工具。
MyCat:中心化代理架构,部署在应用和数据库之间,应用无感知,但 MyCat 自身成了瓶颈点。每个 SQL 经过 MyCat 解析 → 路由 → 下发 → 结果归并,全部走网络,单 MyCat 实例的吞吐上限约 2~3 万 QPS。
真实场景性能数据
在一个 1 亿条订单数据的测试场景中(8 分片 × 2 副本,每分片 1250 万行):
| 场景 | ShardingSphere-JDBC | ShardingSphere-Proxy | MyCat |
|---|---|---|---|
| 主键查询(单分片) | 1.2ms | 3.8ms | 5.1ms |
| 非分片键查询(广播) | 28ms(并行) | 42ms(串行) | 65ms(串行) |
| 批量插入 1000 条 | 120ms | 340ms | 890ms |
| 跨分片 count | 15ms(并行聚合) | 22ms(代理聚合) | 38ms(代理聚合) |
| 连接数消耗 | 1 连接/分片 × 应用实例 | 1 连接/分片 × 代理实例 | 1 连接/分片 × 代理实例 |
ShardingSphere-JDBC 在单分片查询上比 MyCat 快 4 倍以上,关键是省去了代理层的网络往返和序列化开销。
详细对比
| 维度 | ShardingSphere | MyCat |
|---|---|---|
| 架构模式 | 无中心化(JDBC)/ 有中心化(Proxy) | 有中心化代理 |
| 部署复杂度 | 无需额外部署,代码引入即可 | 需要独立部署代理服务 |
| SQL 支持 | 完善(子查询、窗口函数、CTE、JOIN) | 有限(多表 JOIN 不支持,子查询有坑) |
| 性能 | 高(无网络跳转) | 中等(代理层转发增加延迟) |
| 分布式事务 | 集成 Seata、XA、TCC | 有限支持 |
| 生态活跃度 | 高(Apache 基金会,社区活跃,GitHub 23k+ stars) | 低(更新缓慢,GitHub 10k+ stars,最后一次大版本 2020) |
| 应用侵入 | 需要修改数据源配置 | 无侵入 |
| 跨语言支持 | JDBC 模式仅 Java;Proxy 模式支持任意语言 | 任意语言 |
代码示例:Sharding-JDBC 配置
# application.yml
spring:
shardingsphere:
datasource:
names: ds0, ds1
ds0:
url: jdbc:mysql://localhost:3306/order_db_0
username: root
password: root
ds1:
url: jdbc:mysql://localhost:3306/order_db_1
username: root
password: root
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..15}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order-id-mod
key-generate-strategy:
column: order_id
key-generator-name: snowflake
sharding-algorithms:
order-id-mod:
type: MOD
props:
sharding-count: 16
key-generators:
snowflake:
type: SNOWFLAKE代码示例:ShardingSphere 分片算法(Java 自定义)
// 自定义分片算法:按用户 ID 取模分库,按订单时间范围分表
public class MyShardingAlgorithm implements StandardShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
// 按用户 ID 取模确定目标库
long userId = shardingValue.getValue();
int dbIndex = (int) (userId % 2);
return "ds" + dbIndex;
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Long> shardingValue) {
// 按 ID 范围确定目标表
Range<Long> range = shardingValue.getValueRange();
long lower = range.lowerEndpoint();
long upper = range.upperEndpoint();
// 计算涉及的表范围
int minTable = (int) (lower % 16);
int maxTable = (int) (upper % 16);
// 构造表名集合
// ...
return result;
}
}注意:自定义算法类必须实现 StandardShardingAlgorithm 接口的两个 doSharding 方法——精确查询(PreciseShardingValue)和范围查询(RangeShardingValue)。如果只实现了精确查询,业务中一旦出现 WHERE order_id BETWEEN 1000 AND 2000 就会直接报错。这算是一个常见的坑。
代码示例:ShardingSphere 跨分片分页的坑
// 错误直觉:分页查询跨分片后,limit 10 offset 1000000 会先在各分片取 1000010 条,再归并排序
// 实际:ShardingSphere 不会优化 offset,它老老实实从每个分片拉 1000010 条,归并后丢弃前 1000000 条
// 性能:假设 16 分片,每张表 125 万行,order by time desc limit 10 offset 1000000
// 每次查询拉取 1000010 × 16 = 1600 万条记录在内存归并排序,CPU 飙升,GC 频繁
// 正确做法:用游标 / 滚动分页,记录上一页最后一条的 order_id
String sql = "SELECT * FROM t_order WHERE order_id > ? ORDER BY order_id LIMIT 10";
// 而不是 limit 10 offset 1000000四、分片键选择的核心原则
分片键选错了,整个分库分表方案就废了。选型的核心原则:
选择查询频率最高的列作为分片键。用户 ID、订单 ID 是常见选择。如果多种查询维度都需要,需要引入二级索引方案(Elasticsearch 或额外映射表)。
避免热点分片。按时间取模会导致热点数据集中在最近的分片,老数据几乎不会被访问。按时间范围分片更合理:新数据写入当天的表,旧数据冷存。
跨分片查询越少越好。分片键一旦确定,非分片键的查询就是跨分片扫描(广播查询),性能极差。以 32 分片为例,一条
SELECT * FROM t_order WHERE user_name = 'xxx'会在 32 个分片各执行一次,返回 32 个结果集再归并。
踩坑案例:分片键选错
某支付公司早期的分片方案:用 order_id(取模) 做分片键。上线后发现运营后台查询 SELECT * FROM t_order WHERE merchant_id = 12345 ORDER BY create_time DESC LIMIT 20 需要广播到 128 个分片,返回 128 × 20 条数据在应用层排序,耗时 3~5 秒。后来加了 ES 做二级索引,才把查询降到 50ms 以内。
五、ShardingSphere 的扩容方案
取模分片从 8 库扩到 16 库,数据需要全量迁移,这是最头疼的问题。
几种解决思路:
- 一致性哈希:扩容时只迁移 1/16 的数据到新节点,而不是全部重排
- 时间范围分片:按时间分片天然支持扩容,加新表即可,不需要迁移
- 预分片:一开始就按 1024 个分片建表,先用 16 个库,后续加库时用迁移工具重新分布
- 双写方案:扩容期间,新旧两套分片同时写入,读路由到旧库,数据迁移完成后切换读路由
双写扩容的流程
Step 1: 部署新库 + 新分片规则
Step 2: 应用启动双写(旧库 + 新库),读仍然走旧库
Step 3: 后台迁移任务从旧库逐批扫描数据,写入新库(按新分片路由)
Step 4: 校验数据一致性(新旧 count 对比 + 抽样校验)
Step 5: 切换读路由到新库,观察一段时间无异常后下线旧库双写期间,写入 TPS 会下降 30%~50%(因为一次写入变两次),如果业务对延迟敏感,需要在写入层做异步双写或 MQ 削峰。
六、分库分表不是银弹
如果数据量还在 5000 万以下,先考虑这些方案,别急着上分库分表:
- MySQL 垂直分区(按字段拆分宽表)
- 索引优化 + 慢查询治理
- 缓存层(Redis 扛读热点)
- 读写分离
- 冷热数据分离(归档历史数据)
分库分表带来的复杂度——跨分片查询、分布式事务、全局主键、数据迁移、扩容——每一个都是大坑。大多数公司根本不需要分库分表,需要的是先把索引和慢查询搞好。
七、总结
- ShardingSphere 是当前主流,推荐 Sharding-JDBC 模式,性能好、SQL 支持完善
- MyCat 中心化架构的瓶颈明显,生态已落后,新项目不推荐
- 分片键选型是整个方案成败的关键,多花时间评估
- 跨分片分页的 offset 性能问题是隐藏的坑,用游标分页代替
- 能用索引和缓存解的问题,就别上分库分表
参考:ShardingSphere 官方文档(https://shardingsphere.apache.org/);《高性能 MySQL》第 11 章分片