ShardingSphere 深入原理
分库分表主流解决方案。核心能力:将一条 SQL 查询分散到多个分片中执行,再合并结果。对业务代码透明,你写的还是单表 SQL,实际被框架拆分执行。
1. 为什么需要分库分表
1.1 单库单表的瓶颈
业务增长,数据量超过千万级:
| 问题 | 表现 |
|---|---|
| 查询慢 | 全表扫描,索引失效 |
| 写入慢 | 锁竞争,TPS 下降 |
| 存储爆 | 单机磁盘不够 |
| 连接耗尽 | 数据库连接数上限 |
单表超过 2000 万行,DDL 执行时间可能超过分钟级,业务不可用。
1.2 解决思路
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 读从分离 | 主写从读,复制延迟 | 读多写少 |
| 分库分表 | 数据分散到多个库/表 | 写也多、数据量大 |
| NewSQL | 分布式 SQL 引擎(如 TiDB) | 不想改 SQL |
ShardingSphere 属于分库分表方案。
2. 分片核心概念
2.1 垂直分片 vs 水平分片
垂直分片(纵向):按业务模块拆分
原始库:用户库 + 订单库 + 商品库
↓ 拆分
用户库(users) 订单库(orders) 商品库(products)
类比:把一个大货架按类别分成多个小货架,每个货架放一类商品。
水平分片(横向):按数据行数拆分
原始表:orders(1亿行)
↓按 user_id 拆分
orders_0(3000万) orders_1(3000万) orders_2(3000万)
类比:一张大桌子坐不下,把人分散到多张桌子上,每张桌子数据结构完全相同。
2.2 分片键(Shard Key)
决定数据分散到哪个分片的字段。
spring:
shardingsphere:
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds_${0..1}.t_order_${0..3}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order_id_mod
order_id 就是分片键。ShardingSphere 根据它的值决定落在哪个分片。
2.3 路由算法
| 算法 | 原理 | 适用场景 |
|---|---|---|
Mod |
取模 order_id % 4 |
均匀分布 |
Hash |
哈希散列 | 离散度高 |
Range |
范围 ID 0~1000→表0 |
时间序列 |
Key |
自定义 | 特殊业务逻辑 |
取模算法的坑:扩容时数据全部需要迁移。
当前 4 表:order_id % 4 = 0,1,2,3 对应表0~3
扩容到 8 表:order_id % 8 = 0~7
原来余0的数据现在可能余4,数据要重新分配!
解决方案:使用一致性哈希或翻倍扩容(4→8 的过程中先迁移 50%)。
3. ShardingSphere 架构
3.1 架构图
业务代码(JDBC)
↓
ShardingSphere-JDBC(Java 客户端库,轻量级)
↓
SQL 解析 → SQL 改写 → SQL 路由 → 并行执行 → 结果合并
↓
DataSource(多个真实数据源)
3.2 核心处理流程
Step 1:SQL 解析 将你的 SQL 解析成 AST(抽象语法树)
SELECT * FROM t_order WHERE order_id = 1000
↓ 解析
Token: SELECT, FROM(t_order), WHERE(order_id = 1000)
Step 2:SQL 改写 根据分片策略,改写 SQL 中的逻辑表名 → 真实表名
-- 逻辑 SQL(你写的)
SELECT * FROM t_order WHERE order_id = 1000
-- 改写后的真实 SQL
SELECT * FROM t_order_2 WHERE order_id = 1000 (假设路由到表2)
Step 3:SQL 路由 根据分片键计算应该去哪个分片执行
// 路由算法示例
tableIndex = hash(order_id) % 4
真实表名 = t_order_${tableIndex}
Step 4:并行执行 同时向多个分片发起查询
查询请求 → 并行分发 → ds_0.t_order_0
→ ds_0.t_order_1
→ ds_1.t_order_2
→ ds_1.t_order_3
↓
汇总结果
Step 5:结果合并 将各分片返回的结果按照排序、分页逻辑合并
// 分页合并示例:limit 10, offset 100
// 原本需要从每个分片取 110 条,聚合后取前 10 条
// ShardingSphere 会优化:每个分片取 110 条 → 取 35 条(分片数分摊)
4. 分片配置详解
4.1 数据节点配置
spring:
shardingsphere:
rules:
sharding:
tables:
t_order:
# 格式:数据源名.逻辑表名
# ds_${0..1} 表示 ds_0 和 ds_1 两个数据源
# t_order_${0..3} 表示 4 张分片表
actual-data-nodes: ds_${0..1}.t_order_${0..3}
这行配置的含义:数据分散在 ds_0 和 ds_1 两个数据源的 t_order_0 ~ t_order_3 共 8 张表中。
4.2 分片算法配置
rules:
sharding:
sharding-algorithms:
order_id_mod: # 自定义算法名
type: INLINE
props:
algorithm-expression: "t_order_${order_id % 4}"
order_time_range:
type: CLASS_BASED
props:
strategy: STANDARD
algorithmClassName: com.example.TimeRangeShardingAlgorithm
4.3 分片策略
tables:
t_order:
database-strategy: # 库级分片策略
standard:
sharding-column: user_id
sharding-algorithm-name: user_id_mod
table-strategy: # 表级分片策略
standard:
sharding-column: order_id
sharding-algorithm-name: order_id_mod
user_id决定落在哪个库order_id决定落在哪个表
5. 读写分离(Read-Write Splitting)
与分片配合使用,读取压力分摊到从库:
spring:
shardingsphere:
rules:
readwrite-splitting:
data-sources:
ds_master:
write-data-source-name: ds_0
read-data-sources: ds_1, ds_2 # 一主多从
replica-strategy:
load-balance-algorithm: ROUND_ROBIN
关键点:主库负责写 + 强一致读,从库负责普通读。
但这引入了复制延迟问题:写入主库后立刻查询,可能查到旧数据。
6. 分布式 ID 生成
分片后自增 ID 冲突,需要分布式 ID 方案:
| 方案 | 实现 | 优点 | 缺点 |
|---|---|---|---|
| UUID | UUID.randomUUID() |
简单 | 无序、占用空间大 |
| 数据库自增 | 单独 ID 生成表 | 有序 | 强依赖 DB,有瓶颈 |
| Redis INCR | INCR key |
性能好 | 依赖 Redis |
| Snowflake | 时间戳 + 机器ID + 序列号 | 有序、不依赖 DB | 需要时钟同步 |
Snowflake 原理:
64bit = 1bit(符号位) + 41bit(时间戳) + 10bit(机器ID) + 12bit(序列号)
- 时间戳:支持约 69 年
- 机器 ID:1024 台机器
- 序列号:每毫秒每机器最多 4096 个 ID
7. 分页查询的坑
7.1 全局分页问题
-- 逻辑 SQL:查询第 100 页,每页 10 条
SELECT * FROM t_order ORDER BY order_id LIMIT 1000, 10
假设 4 个分片各返回前 10010 条数据,合并后再排序取第 1000~1010 条。
问题:分片越多,每个分片需要返回的数据越多,内存压力越大。
7.2 解决方案:游标分页
不用 offset,使用 last_id:
-- 第一页
SELECT * FROM t_order WHERE order_id > 0 ORDER BY order_id LIMIT 10
-- 返回 last_id = 10010
-- 第二页(上一页最后一条的 order_id)
SELECT * FROM t_order WHERE order_id > 10010 ORDER BY order_id LIMIT 10
ShardingSphere 5.x 支持 SHARDING_SPHERE_KEY 游标分页。
8. 跨分片聚合运算
-- 跨 4 个分片统计订单总数和总金额
SELECT COUNT(*), SUM(amount) FROM t_order WHERE user_id = 100
ShardingSphere 处理方式:
ds_0.t_order_0 → 返回 count=5, sum=500
ds_0.t_order_1 → 返回 count=3, sum=300
ds_1.t_order_2 → 返回 count=8, sum=800
ds_1.t_order_3 → 返回 count=4, sum=400
↓
最终结果合并:count=20, sum=2000
限制:跨分片的 JOIN、GROUP BY 非常复杂,ShardingSphere 支持有限,尽量避免。
9. 常见问题
9.1 扩容迁移
不停服扩容的标准流程:
- 双写:新旧两套分片策略同时生效
- 数据同步:用 CDC(Change Data Capture)工具同步历史数据
- 灰度切读:逐步将读流量切换到新分片
- 切写:全部流量切换到新分片
- 下线旧分片
9.2 跨分片事务
ShardingSphere 默认不支持强一致性跨分片事务(XA 模式性能差)。实际项目中的最佳实践:
- 最终一致性:基于消息队列做异步补偿
- TCC:两阶段提交,业务侧实现 try/confirm/cancel
- Seata:阿里的分布式事务框架,与 ShardingSphere 可集成
9.3 分片键选择原则
- 查询高频字段优先(如
user_id、order_id) - 基数足够大(取值种类要多)
- 避免跨分片查询(业务上尽量按分片键做条件)
- 写入要分散(避免热点 key 集中在某个分片)
如果查询条件不带分片键(如查所有订单),ShardingSphere 会广播到所有分片执行(Scatter-Gather),分片越多性能越差。