Skip to content

分库分表方案: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-JDBCShardingSphere-ProxyMyCat
主键查询(单分片)1.2ms3.8ms5.1ms
非分片键查询(广播)28ms(并行)42ms(串行)65ms(串行)
批量插入 1000 条120ms340ms890ms
跨分片 count15ms(并行聚合)22ms(代理聚合)38ms(代理聚合)
连接数消耗1 连接/分片 × 应用实例1 连接/分片 × 代理实例1 连接/分片 × 代理实例

ShardingSphere-JDBC 在单分片查询上比 MyCat 快 4 倍以上,关键是省去了代理层的网络往返和序列化开销。

详细对比

维度ShardingSphereMyCat
架构模式无中心化(JDBC)/ 有中心化(Proxy)有中心化代理
部署复杂度无需额外部署,代码引入即可需要独立部署代理服务
SQL 支持完善(子查询、窗口函数、CTE、JOIN)有限(多表 JOIN 不支持,子查询有坑)
性能高(无网络跳转)中等(代理层转发增加延迟)
分布式事务集成 Seata、XA、TCC有限支持
生态活跃度高(Apache 基金会,社区活跃,GitHub 23k+ stars)低(更新缓慢,GitHub 10k+ stars,最后一次大版本 2020)
应用侵入需要修改数据源配置无侵入
跨语言支持JDBC 模式仅 Java;Proxy 模式支持任意语言任意语言

代码示例:Sharding-JDBC 配置

yaml
# 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 自定义)

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 跨分片分页的坑

java
// 错误直觉:分页查询跨分片后,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

四、分片键选择的核心原则

分片键选错了,整个分库分表方案就废了。选型的核心原则:

  1. 选择查询频率最高的列作为分片键。用户 ID、订单 ID 是常见选择。如果多种查询维度都需要,需要引入二级索引方案(Elasticsearch 或额外映射表)。

  2. 避免热点分片。按时间取模会导致热点数据集中在最近的分片,老数据几乎不会被访问。按时间范围分片更合理:新数据写入当天的表,旧数据冷存。

  3. 跨分片查询越少越好。分片键一旦确定,非分片键的查询就是跨分片扫描(广播查询),性能极差。以 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 章分片

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。