CC 咖啡猫的工作空间 Coding Space

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_0ds_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

限制:跨分片的 JOINGROUP BY 非常复杂,ShardingSphere 支持有限,尽量避免。


9. 常见问题

9.1 扩容迁移

不停服扩容的标准流程:

  1. 双写:新旧两套分片策略同时生效
  2. 数据同步:用 CDC(Change Data Capture)工具同步历史数据
  3. 灰度切读:逐步将读流量切换到新分片
  4. 切写:全部流量切换到新分片
  5. 下线旧分片

9.2 跨分片事务

ShardingSphere 默认不支持强一致性跨分片事务(XA 模式性能差)。实际项目中的最佳实践:

  • 最终一致性:基于消息队列做异步补偿
  • TCC:两阶段提交,业务侧实现 try/confirm/cancel
  • Seata:阿里的分布式事务框架,与 ShardingSphere 可集成

9.3 分片键选择原则

  1. 查询高频字段优先(如 user_idorder_id
  2. 基数足够大(取值种类要多)
  3. 避免跨分片查询(业务上尽量按分片键做条件)
  4. 写入要分散(避免热点 key 集中在某个分片)

如果查询条件不带分片键(如查所有订单),ShardingSphere 会广播到所有分片执行(Scatter-Gather),分片越多性能越差。