分库分表中间件的双雄对决 ⚔️

64 阅读8分钟

一、开篇故事:快递分拣中心的两种方案 📦

想象你要建一个快递分拣系统,有两种方案:

方案1:Sharding-JDBC(应用层方案)💼

快递员(应用)自己决定送到哪个仓库:

快递员:"这个包裹发往北京..."
快递员:(查地图)"北京归1号仓库管!"
快递员:直接送到1号仓库 ✅

特点:
  - 快递员智能(应用程序内置路由逻辑)
  - 不需要中转站
  - 效率高!

方案2:MyCat(代理层方案)🚀

所有快递先送到中转站(代理),中转站分配:

快递员:"这个包裹麻烦帮我分拣。"
中转站:"好的,我看看...北京的去1号仓库!"
中转站:转发到1号仓库 ✅

特点:
  - 快递员简单(不需要知道规则)
  - 有中转站(多一跳)
  - 方便管理!

二、Sharding-JDBC:客户端分片 📱

2.1 什么是Sharding-JDBC?

Sharding-JDBC(现在叫ShardingSphere-JDBC) 是一个轻量级Java框架,在应用层实现分库分表。

2.2 核心特点

特点:
1. 客户端分片(应用内嵌)
2. 以Jar包形式提供服务
3. 无需额外部署
4. 支持任何ORM框架
5. 性能损耗极小(< 2%)

2.3 架构图

应用层
  ├─ Service
  ├─ Sharding-JDBC(路由、改写、归并)
  ├─ JDBC Driver
  └─ 直接连接数据库

      ↓  ↓  ↓  ↓
   DB1 DB2 DB3 DB4

没有中间层!直连数据库!


2.4 工作流程

步骤1SQL解析
  SELECT * FROM orders WHERE user_id = 12345;
  → 解析成AST(抽象语法树)

步骤2SQL路由
  user_id = 12345
  → 计算:12345 % 4 = 1
  → 路由到:db1.orders_1

步骤3SQL改写
  原SQLSELECT * FROM orders WHERE user_id = 12345;
  改写后:SELECT * FROM orders_1 WHERE user_id = 12345;

步骤4SQL执行
  → 发送到db1.orders_1
  → 获取结果

步骤5:结果归并
  如果查询多个分片,归并结果
  → 返回给应用

2.5 配置示例

// 1. 配置数据源
Map<String, DataSource> dataSourceMap = new HashMap<>();
dataSourceMap.put("db0", createDataSource("jdbc:mysql://localhost:3306/db0"));
dataSourceMap.put("db1", createDataSource("jdbc:mysql://localhost:3306/db1"));

// 2. 配置分片规则
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();

// 表分片规则
TableRuleConfiguration orderTableRuleConfig = new TableRuleConfiguration("orders", "db${0..1}.orders_${0..1}");

// 分库策略(按user_id % 2)
orderTableRuleConfig.setDatabaseShardingStrategyConfig(
    new InlineShardingStrategyConfiguration("user_id", "db${user_id % 2}")
);

// 分表策略(按user_id % 2)
orderTableRuleConfig.setTableShardingStrategyConfig(
    new InlineShardingStrategyConfiguration("user_id", "orders_${user_id % 2}")
);

shardingRuleConfig.getTableRuleConfigs().add(orderTableRuleConfig);

// 3. 创建数据源
DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties());

// 4. 使用(和普通JDBC一样)
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE user_id = ?");
ps.setLong(1, 12345);
ResultSet rs = ps.executeQuery();

2.6 优点 ✅

✅ 性能损耗极小(应用直连数据库)
✅ 无需额外部署(Jar包形式)
✅ 支持任何ORM(MyBatis、JPA等)
✅ 支持多种数据库(MySQL、PostgreSQL等)
✅ 支持读写分离
✅ 支持分布式事务(Seata集成)

2.7 缺点 ❌

❌ 应用需要升级(引入Jar包)
❌ 每个应用都要配置(配置冗余)
❌ 不支持跨语言(只支持Java)
❌ 应用重启才能生效配置

三、MyCat:代理层分片 🌐

3.1 什么是MyCat?

MyCat 是一个数据库中间件,在代理层实现分库分表,对应用透明。

3.2 核心特点

特点:
1. 代理层分片(独立部署)
2. 对应用透明(应用认为是单库)
3. 支持多语言(任何JDBC客户端)
4. 集中式管理
5. 支持MySQL协议

3.3 架构图

应用层
  ├─ Service
  ├─ JDBC Driver
  └─ 连接MyCat(端口8066)
        ↓
     MyCat(代理层)
      ├─ SQL解析
      ├─ 路由
      ├─ 改写
      └─ 归并
        ↓  ↓  ↓  ↓
     DB1 DB2 DB3 DB4

有中间层!应用连接MyCat,MyCat连接数据库!


3.4 工作流程

步骤1:应用连接MyCat
  应用:连接jdbc:mysql://mycat:8066/mydb
  → 应用以为连接的是MySQL ✅

步骤2:执行SQL
  应用:SELECT * FROM orders WHERE user_id = 12345;
  → 发送给MyCat

步骤3:MyCat解析路由
  MyCat:解析SQL
  MyCat:user_id = 12345 → 路由到db1.orders_1

步骤4:MyCat改写SQLSQLSELECT * FROM orders WHERE user_id = 12345;
  改写后:SELECT * FROM orders_1 WHERE user_id = 12345;

步骤5:MyCat执行SQL
  MyCat → db1.orders_1
  → 获取结果

步骤6:MyCat归并结果
  → 返回给应用

3.5 配置示例

server.xml(MyCat服务配置):

<user name="root">
    <property name="password">123456</property>
    <property name="schemas">mydb</property>
</user>

schema.xml(分片规则):

<!-- 逻辑库 -->
<schema name="mydb" checkSQLschema="false" sqlMaxLimit="100">
    <!-- 逻辑表 -->
    <table name="orders" dataNode="dn1,dn2" rule="mod-long" />
</schema>

<!-- 数据节点 -->
<dataNode name="dn1" dataHost="localhost1" database="db1" />
<dataNode name="dn2" dataHost="localhost2" database="db2" />

<!-- 数据主机 -->
<dataHost name="localhost1" maxCon="1000" minCon="10" balance="0">
    <heartbeat>select user()</heartbeat>
    <writeHost host="hostM1" url="192.168.1.100:3306" user="root" password="123456" />
</dataHost>

rule.xml(路由规则):

<tableRule name="mod-long">
    <rule>
        <columns>user_id</columns>
        <algorithm>mod-long</algorithm>
    </rule>
</tableRule>

<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
    <property name="count">2</property> <!-- 分片数量 -->
</function>

应用使用:

// 应用连接MyCat(和连接MySQL一样)
Connection conn = DriverManager.getConnection(
    "jdbc:mysql://mycat:8066/mydb", "root", "123456"
);

// 正常执行SQL(不需要关心分片)
PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE user_id = ?");
ps.setLong(1, 12345);
ResultSet rs = ps.executeQuery();

3.6 优点 ✅

✅ 对应用透明(无需修改代码)
✅ 集中式管理(一处配置,全局生效)
✅ 支持多语言(任何MySQL客户端)
✅ 支持读写分离
✅ 支持全局表(字典表)
✅ 动态配置(reload即可生效)

3.7 缺点 ❌

❌ 多一层代理(性能损耗约5-10%)
❌ 需要额外部署维护
❌ 单点故障风险(需要MyCat集群)
❌ 网络延迟增加
❌ 复杂SQL支持有限

四、Sharding-JDBC vs MyCat 对决 ⚔️

4.1 全面对比表

特性Sharding-JDBCMyCat
架构应用层(客户端)代理层(独立服务)
部署Jar包(无需部署)独立部署
性能损耗< 2% ✅5-10% ⚠️
对应用侵入有(需引入Jar)无(透明)✅
多语言支持❌ 只支持Java✅ 支持任何语言
配置管理分散(每个应用)集中(统一配置)✅
运维复杂度低 ✅中(需维护MyCat)
单点故障❌ 无(分散)✅ 有(需集群)
SQL支持好 ✅中(复杂SQL受限)
分布式事务支持(Seata)支持(弱XA)
读写分离支持 ✅支持 ✅
动态配置❌ 需重启✅ reload即可

4.2 性能对比

测试场景: 单表查询,10000次

直连MySQL:
  平均耗时:1ms
  QPS:10000

Sharding-JDBC:
  平均耗时:1.02ms(增加2%)
  QPS:9800 ✅

MyCat:
  平均耗时:1.1ms(增加10%)
  QPS:9000 ⚠️

4.3 适用场景

Sharding-JDBC适合:

✅ Java技术栈项目
✅ 追求极致性能
✅ 微服务架构(每个服务独立分片)
✅ 不想额外部署中间件
✅ 团队有Java经验

典型场景:
  - 互联网电商(高并发)
  - 金融系统(高性能要求)
  - 微服务架构

MyCat适合:

✅ 多语言技术栈(Java、Python、Go等)
✅ 希望对应用透明
✅ 集中式管理
✅ 传统单体应用改造
✅ 团队Java经验不足

典型场景:
  - 遗留系统改造
  - 多语言混合项目
  - 需要集中管理

五、分库分表路由策略 🗺️

5.1 Hash取模

// 分库分表:user_id % 4
database_index = user_id % 4
table_index = user_id / 1000000 % 4

示例:
  user_id = 12345678
  → database_index = 12345678 % 4 = 2(db2)
  → table_index = 12345678 / 1000000 % 4 = 2(orders_2)
  → 路由到:db2.orders_2 ✅

优点:

✅ 数据分布均匀
✅ 负载均衡

缺点:

❌ 扩容麻烦(需要重新Hash)
❌ 范围查询需要查所有库表

5.2 范围分片

// 按ID范围分库
if (user_id < 10000000) {
    database = "db0";
} else if (user_id < 20000000) {
    database = "db1";
} else {
    database = "db2";
}

示例:
  user_id = 5000000 → db0 ✅
  user_id = 15000000 → db1 ✅

优点:

✅ 范围查询方便
✅ 扩容方便(新增范围)

缺点:

❌ 数据分布可能不均(热点问题)

5.3 时间分片

// 按月分表
table_name = "orders_" + create_time.format("yyyyMM");

示例:
  2024-01-15 → orders_202401 ✅
  2024-02-10 → orders_202402 ✅

优点:

✅ 适合时间范围查询
✅ 历史数据归档方便

5.4 一致性Hash

// 使用一致性Hash
int hash = hash(user_id);
Node node = consistentHash.getNode(hash);

优点:
  ✅ 扩容时数据迁移量小(只迁移1/N)
  ✅ 负载均衡

六、跨库查询解决方案 🔗

问题1:跨库JOIN

-- ❌ 无法执行
SELECT * FROM db1.users u
JOIN db2.orders o ON u.id = o.user_id;

解决方案:

方案1:应用层JOIN

// 1. 分别查询
List<User> users = userService.getUsers(userIds);
List<Order> orders = orderService.getOrders(userIds);

// 2. 应用层关联
Map<Long, User> userMap = users.stream()
    .collect(Collectors.toMap(User::getId, u -> u));

orders.forEach(order -> {
    User user = userMap.get(order.getUserId());
    order.setUser(user);
});

方案2:数据冗余

-- 在orders表冗余user_name等字段
CREATE TABLE orders (
    id BIGINT,
    user_id BIGINT,
    user_name VARCHAR(50), -- 冗余
    user_phone VARCHAR(20), -- 冗余
    ...
);

方案3:全局表

Sharding-JDBC:

// 配置全局表(每个库都复制一份)
TableRuleConfiguration globalTableConfig = new TableRuleConfiguration("dict", "db${0..1}.dict");
globalTableConfig.setBroadcastTable(true); // 广播表

MyCat:

<table name="dict" dataNode="dn1,dn2" type="global" />

问题2:分布式事务

// ❌ 无法保证
@Transactional
public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) {
    // from用户在db1
    accountService.deduct(fromUserId, amount);
    
    // to用户在db2
    accountService.add(toUserId, amount);
    
    // 如果db2失败,db1已扣款!💀
}

解决方案:

方案1:Seata(推荐)

// Sharding-JDBC集成Seata
@GlobalTransactional
public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) {
    accountService.deduct(fromUserId, amount);
    accountService.add(toUserId, amount);
    // Seata保证分布式事务一致性 ✅
}

方案2:本地消息表

@Transactional
public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) {
    // 1. 扣款 + 记录消息表
    accountService.deduct(fromUserId, amount);
    messageTable.insert("transfer", toUserId, amount);
    
    // 2. 定时任务处理消息表
    // 3. 加款成功,删除消息
}

七、实战案例:电商订单分库分表 💼

需求

订单表:5000万数据
分片策略:
  - 4个库
  - 每库4个表
  - 共16个分片
  - 按user_id Hash分片

Sharding-JDBC实现

# application.yml
spring:
  shardingsphere:
    datasource:
      names: db0,db1,db2,db3
      db0:
        type: com.zaxxer.hikari.HikariDataSource
        url: jdbc:mysql://localhost:3306/db0
        username: root
        password: 123456
      db1:
        url: jdbc:mysql://localhost:3306/db1
        # ...
    
    rules:
      sharding:
        tables:
          orders:
            actual-data-nodes: db${0..3}.orders_${0..3}
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: database-mod
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: table-mod
        
        sharding-algorithms:
          database-mod:
            type: MOD
            props:
              sharding-count: 4
          table-mod:
            type: MOD
            props:
              sharding-count: 4

使用:

@Mapper
public interface OrderMapper {
    // 正常写MyBatis,Sharding-JDBC自动路由
    @Select("SELECT * FROM orders WHERE user_id = #{userId}")
    List<Order> findByUserId(Long userId);
}

八、面试高频问题 🎤

Q1: Sharding-JDBC和MyCat的区别?

答:

  • Sharding-JDBC:客户端分片,应用层,性能好,只支持Java
  • MyCat:代理层分片,对应用透明,支持多语言,有性能损耗

Q2: 如何选择分库分表中间件?

答:

  • Java项目 + 追求性能 → Sharding-JDBC
  • 多语言 + 对应用透明 + 集中管理 → MyCat

Q3: 分库分表后如何解决跨库JOIN?

答:

  1. 应用层JOIN(分别查询,应用层关联)
  2. 数据冗余(在表中冗余关联字段)
  3. 全局表(字典表在每个库都复制)
  4. 使用ES等搜索引擎

Q4: 分库分表后如何解决分布式事务?

答:

  1. Seata(推荐)
  2. 本地消息表
  3. TCC
  4. Saga

Q5: 分库分表后如何扩容?

答:

  1. 一致性Hash:迁移量小
  2. 翻倍扩容:2库→4库→8库,迁移一半数据
  3. 停机扩容:全量迁移(适合小数据量)

九、总结口诀 📝

分库分表有两派,
Sharding和MyCat。
Sharding客户端,
性能好不用部署。

MyCat是代理,
透明化集中管。
Java用Sharding,
多语言选MyCat。

路由策略要选好,
Hash、范围、时间。
跨库JOIN要避免,
应用层来解决。

分布式事务难,
Seata来帮忙。
配置好规则后,
性能提升看得见!

参考资料 📚


本批次完成! 🎉
已完成文档141-145(共5个)!


编写时间:2025年
作者:技术文档小助手 ✍️
版本:v1.0

愿你的分库分表如丝般顺滑! 🔧✨