一、开篇故事:快递分拣中心的两种方案 📦
想象你要建一个快递分拣系统,有两种方案:
方案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 工作流程
步骤1:SQL解析
SELECT * FROM orders WHERE user_id = 12345;
→ 解析成AST(抽象语法树)
步骤2:SQL路由
user_id = 12345
→ 计算:12345 % 4 = 1
→ 路由到:db1.orders_1
步骤3:SQL改写
原SQL:SELECT * FROM orders WHERE user_id = 12345;
改写后:SELECT * FROM orders_1 WHERE user_id = 12345;
步骤4:SQL执行
→ 发送到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改写SQL
原SQL:SELECT * 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-JDBC | MyCat |
|---|---|---|
| 架构 | 应用层(客户端) | 代理层(独立服务) |
| 部署 | 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?
答:
- 应用层JOIN(分别查询,应用层关联)
- 数据冗余(在表中冗余关联字段)
- 全局表(字典表在每个库都复制)
- 使用ES等搜索引擎
Q4: 分库分表后如何解决分布式事务?
答:
- Seata(推荐)
- 本地消息表
- TCC
- Saga
Q5: 分库分表后如何扩容?
答:
- 一致性Hash:迁移量小
- 翻倍扩容:2库→4库→8库,迁移一半数据
- 停机扩容:全量迁移(适合小数据量)
九、总结口诀 📝
分库分表有两派,
Sharding和MyCat。
Sharding客户端,
性能好不用部署。
MyCat是代理,
透明化集中管。
Java用Sharding,
多语言选MyCat。
路由策略要选好,
Hash、范围、时间。
跨库JOIN要避免,
应用层来解决。
分布式事务难,
Seata来帮忙。
配置好规则后,
性能提升看得见!
参考资料 📚
本批次完成! 🎉
已完成文档141-145(共5个)!
编写时间:2025年
作者:技术文档小助手 ✍️
版本:v1.0
愿你的分库分表如丝般顺滑! 🔧✨