简介:面向SpringBoot开发者的ShardingSphere 5.2.1整合示例包,聚焦“分表不分库”场景,可在单一数据库实例中将大表拆分为多张分片表,缓解单表数据量过大引发的查询性能问题,适用于持续增长的业务表拆分需求。压缩包共16个文件,约15KB,以9个Java类为主,涵盖分片规则构建、数据源配置、事务管理及自定义分片算法等核心代码;另含2个YAML配置文件用于数据源与分片策略声明,2个XML文件补充框架装配信息,并有SQL脚本、.gitignore等辅助内容,结构紧凑,便于直接对照实操。目前已有1953人学习下载。内容从依赖引入、规则配置讲到数据迁移与SQL优化,给出范围分片、虚拟节点分片、哈希分片及自定义分片算法等策略写法,并针对分页查询、分布式事务等实践要点进行说明,适合正在调研分表方案或准备在项目中落地的中级Java工程师参考。
1. Spring Boot 整合 ShardingSphere 5.2.1:先搞清楚这一步到底解决什么
Spring Boot 项目要做到分库分表,当前比较稳的路线就是整合 ShardingSphere 5.2.1。很多第一次做整合的开发者会遇到同一个拦路虎:配置写好后,SQL 日志看着像拆开了,实际数据还是落在单库单表上,甚至应用直接启动报错。这篇笔记就围绕 5.2.1 把整合路径讲透——从依赖引入、YAML 分片规则、读写分离,到分布式主键、深翻页这几个容易翻车的点,按真实业务会遇到的顺序走一遍。适合正在选型分库分表、或者已经加入 ShardingSphere 但不敢上生产的人。看完你能照着配出一个最小可用工程,也知道每项参数为什么这么设。5.2.1 在 5.x 里不是最新小版本,但属于社区验证最扎实的版本之一,拿它当生产基准版本,比盲目追后续小版本更省心。
2. 数据分片配置:依赖、YAML 规则与分片算法的选型清单
数据分片不是“写在配置里就自动生效”的,它依赖三个约定:逻辑表、数据源、分片算法,三者名字和表达式必须完全对得上。这一章从依赖开始,一步步把最小可用的双库双表分片搭起来。
2.1 引入依赖与数据源:先让 Spring Boot 能连上两个库
先加 ShardingSphere 的 Spring Boot Starter,它会自动创建 ShardingSphereDataSource 并接管数据源路由。如果你的项目是 Spring Boot 2.7.x,用下面的依赖坐标即可:
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-spring-boot-starter</artifactId> <version>5.2.1</version> </dependency>依赖加好后,在application.yml里配置两个真实数据源。这里要特别注意:数据源配置是放在spring.shardingsphere.datasource下面,而不是spring.datasource下面,否则 ShardingSphere 根本不会接管。
spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.10:3306/shop0?useSSL=false&serverTimezone=Asia/Shanghai username: root password: root maximum-pool-size: 20 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.11:3306/shop1?useSSL=false&serverTimezone=Asia/Shanghai username: root password: root maximum-pool-size: 20逻辑说明:names里声明的 ds0、ds1 是逻辑数据源名,分片规则里会引用它们。type明确指定 HikariCP,避免 classpath 里同时存在 Druid 时加载到错误的连接池实现。jdbc-url是 HikariCP 的专用键名,不能写成url,这与 Spring Boot 原生spring.datasource的配置习惯不一样。
参数说明:maximum-pool-size在分片场景下不能按单库经验设成 10,后面避坑章节会展开讲。我习惯先把 rules 留空、只配数据源启动一次,确认两个库都能连上,再加分片规则,这样出问题时容易定位是连接问题还是路由问题。
2.2 分片算法怎么选:INLINE、HASH_MOD 与 COMPLEX 的边界
ShardingSphere 5.2.1 内置了多套分片算法,选型直接决定数据分布是否均匀。下面这张表是我在实际项目里常用的判断标准:
| 算法 | 适用键类型 | 路由行为 | 注意点 |
|---|---|---|---|
| INLINE | 整数或可转为字符串 | 用 Groovy 表达式计算,如order_id % 2 | 表达式写错不会启动报错,但会路由到错误分片 |
| HASH_MOD | 整数或字符串 | 对键做哈希后再取模,分布比裸取模均匀 | 必须配sharding-count,模数变更后老数据无法平滑迁移 |
| COMPLEX | 多个分片键 | 按多个列组合决定路由,支持in条件 | 适合订单按用户+时间双维度查询的场景 |
| INTERVAL | 时间字段 | 按时间区间路由到不同的表 | 适合流水表、日志表,需要配时间范围和分片数 |
先看一个直接把订单表拆成 2 库 2 表的完整配置:
spring: shardingsphere: rules: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order_$->{0..1} database-strategy: standard: sharding-column: order_id sharding-algorithm-name: db-inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: table-inline sharding-algorithms: db-inline: type: INLINE props: algorithm-expression: ds$->{order_id % 2} table-inline: type: INLINE props: algorithm-expression: t_order_$->{order_id % 2}逻辑说明:actual-data-nodes的写法是ds$->{0..1}.t_order_$->{0..1},它展开后是 ds0.t_order_0、ds0.t_order_1、ds1.t_order_0、ds1.t_order_1 四个物理表。database-strategy决定去哪个库,table-strategy决定去哪个表,两者可以独立配置。SQL 里写的t_order是逻辑表名,ShardingSphere 会在运行时把它改写成物理表名。
参数说明:sharding-column必须是查询条件里经常出现的列,否则分片失效;algorithm-expression里的order_id % 2和库表数量强相关,如果以后要扩到 4 库,表达式必须同步改成% 4,且历史数据需要迁移。生产环境我更推荐字符串订单号用HASH_MOD而不是 INLINE,写法改为type: HASH_MOD加props.sharding-count: 2,避免手工处理哈希。
2.3 整合后先做验证:启动日志、路由核对与数据落位检查
配置完成后不要急着写业务代码,先打开 SQL 日志确认路由真的发生了。在 5.2.1 里通过spring.shardingsphere.props.sql-show: true开启:
spring: shardingsphere: props: sql-show: true逻辑说明:sql-show会打印每条逻辑 SQL 和改写后的 Actual SQL。如果日志里只有一条 SQL 且表名还是t_order,说明分片没生效;如果看到t_order_0、t_order_1,说明路由已经工作。
路由确认后,还要验证数据真正散开了。分别在两个物理库上执行下面的核对语句:
-- 在 ds0 库执行 SELECT COUNT(*) AS cnt0 FROM t_order_0; SELECT COUNT(*) AS cnt1 FROM t_order_1; -- 在 ds1 库执行 SELECT COUNT(*) AS cnt2 FROM t_order_0; SELECT COUNT(*) AS cnt3 FROM t_order_1;逻辑说明:插入一批测试数据后,四个物理表的行数应该大致接近。如果某一个表行数为 0 而另一个翻倍,多半是分片算法表达式写错或分片键选得有问题,先回头检查% 2的语义,不要急着查连接池。这一步是投入产出比最高的验证手段,能省下后面排查线上数据倾斜的大量时间。
3. 读写分离整合:负载均衡、事务与强制主库的取舍
数据分片解决的是“数据太多放不下”,读写分离解决的是“读太多扛不住”。两者在 5.2.1 里可以在同一个 ShardingSphereDataSource 下共存,但读写分离的坑比分片更隐蔽,尤其是事务和主从延迟。
3.1 读写分离的最小配置与负载均衡算法对比
在上一章分片配置基础上,新增两个从库数据源,然后声明读写分离规则。下面是一个主一从的最小配置:
spring: shardingsphere: datasource: names: write_ds,read_ds_0,read_ds_1 write_ds: type: com.zaxxer.hikari.HikariDataSource jdbc-url: jdbc:mysql://192.168.1.20:3306/shop?useSSL=false username: root password: root read_ds_0: type: com.zaxxer.hikari.HikariDataSource jdbc-url: jdbc:mysql://192.168.1.21:3306/shop?useSSL=false username: root password: root read_ds_1: type: com.zaxxer.hikari.HikariDataSource jdbc-url: jdbc:mysql://192.168.1.22:3306/shop?useSSL=false username: root password: root rules: readwrite-splitting: >@Service public class OrderServiceImpl implements OrderService { @Override @Transactional(rollbackFor = Exception.class) public OrderDetail getOrderDetail(Long orderId) { Order order = orderMapper.selectById(orderId); // 走主库 List<OrderItem> items = itemMapper.selectByOrderId(orderId); // 也走主库 return new OrderDetail(order, items); } }逻辑说明:上面这个读操作的方法加了@Transactional,两个查询都会打到主库。如果这类只读方法占了接口的大头,主库压力会随着读流量线性上涨,读写分离形同虚设。原因在于 ShardingSphere 通过事务上下文决定路由,而 Spring 的声明式事务在进入方法时就已经把绑定关系确定了。
解决方式是把读操作和写操作分开:只读方法不加事务注解,或者拆成独立的查询方法;确实需要读一致性的场景,才在短事务内完成。我一般会在代码评审时明确一条规矩:@Transactional只允许出现在写操作方法上,任何只读方法如果加了事务注解,必须说明理由。
3.3 从库延迟兜不住时:强制主库路由的两个常用姿势
从库复制延迟是 MySQL 主从架构的固有现象,写入后立刻读取的接口会拿到旧数据。ShardingSphere 5.2.1 提供了HintManager,可以在代码里临时把当前查询强制到主库,原理是给当前线程的路由上下文打标记。
public Order getFreshOrder(Long orderId) { try (HintManager hintManager = HintManager.getInstance()) { hintManager.setWriteRouteOnly(); return orderMapper.selectById(orderId); } }逻辑说明:setWriteRouteOnly()让紧接着执行的 SQL 走主库。用try-with-resources包裹,HintManager会在代码块结束时自动清理线程上下文,避免路由状态泄漏到线程池的下一次请求。如果不用 try-with-resources,必须在finally里手动调用clear()。
参数说明:强制主库路由只作用于当前线程的下一批 SQL,不会影响其他请求,适合支付回调后立刻查订单状态、登录后立刻查最新用户信息这类强一致场景。这种写法属于“精确制导”,比给整个方法加@Transactional代价小得多,也是我在项目里首选的兜底方案。
4. 分页与主键的整合细节:深翻页改写、雪花 ID 与日志核实
分库分表之后,最影响用户体验的就是分页变慢,最影响数据正确性的就是主键冲突。这两个问题在 5.2.1 里各有各的行为逻辑,理解了改写规则就不会踩坑。
4.1 LIMIT 被改写了:跨分片分页的真实 SQL 形态
ShardingSphere 对分页的处理不是简单地把 SQL 下推到每个分片,而是先改写,再在内存里归并。打开sql-show后,下面这条深分页 SQL:
SELECT * FROM t_order ORDER BY create_time DESC LIMIT 10000, 20;会被改写成类似这样两条实际执行的 SQL:
SELECT * FROM t_order_0 ORDER BY create_time DESC LIMIT 10020; SELECT * FROM t_order_1 ORDER BY create_time DESC LIMIT 10020;逻辑说明:改写后的 LIMIT 从10000, 20变成了10020,每个分片都要查出前 10020 条记录,然后 ShardingSphere 在内存里按create_time做归并排序,丢掉前 10000 条后返回 20 条。分片数越多,内存消耗越大,2 个分片就是 20040 条,10 个分片就是 100200 条。这也是深翻页在分片场景下指数级变慢的根本原因。
4.2 用游标分页绕开深翻页的改写陷阱
把 offset 分页换成游标分页是分片架构下最可靠的方案。游标分页不依赖 OFFset,而是通过上一个数据的位置继续往下查:
-- 第一页 SELECT * FROM t_order WHERE create_time < '2025-01-01 00:00:00' ORDER BY create_time DESC, order_id DESC LIMIT 20; -- 第二页,把上一页最后一条的 create_time 和 order_id 传进来 SELECT * FROM t_order WHERE create_time < '2024-12-31 23:59:59' AND (create_time < ? OR (create_time = ? AND order_id < ?)) ORDER BY create_time DESC, order_id DESC LIMIT 20;逻辑说明:游标分页的 WHERE 条件同时带上排序字段和分片键order_id,ShardingSphere 可以直接定位到目标分片,每个分片只需要查 20 条,内存归并的代价几乎可以忽略。注意排序字段增加了order_id作为次级排序,这样才能保证跨分片归并时记录顺序稳定。
边界说明:游标分页只适合“下一页”场景,如果产品非要支持“跳到第 500 页”,游标分页救不了,只能接受深翻页的代价。另外,分页条件是create_time这种非分片键时,SQL 会被广播到所有分片,所以分页接口在设计之初就要想清楚用户会怎么翻。
4.3 分布式主键:让 ShardingSphere 生成 ID 而不是 MyBatis-Plus
分库分表后不能再依赖数据库自增主键,ShardingSphere 5.2.1 内置了雪花算法生成器。配置方式是先声明 key-generators,再在分片表策略里挂上主键生成策略:
spring: shardingsphere: rules: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order_$->{0..1} key-generate-strategy: column: order_id key-generator-name: snowflake key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1逻辑说明:key-generate-strategy挂在t_order下,配置的column是主键列order_id。当 INSERT 语句里没有显式传order_id时,ShardingSphere 会用雪花算法生成一个 19 位 long 型 ID 填充进去。key-generator-name引用下面key-generators里声明的snowflake。
参数说明:worker-id在多实例部署时非常关键,每个应用实例必须配置不同的 worker-id,否则同一毫秒内不同实例生成的 ID 在高并发下可能重复。单机调试时可以不管它,生产环境建议通过环境变量注入,例如props.worker-id: ${WORKER_ID:1},让运维在部署时按实例编号指定。
注意:如果项目里同时用了 MyBatis-Plus,它的 ID 生成策略默认是 ASSIGN_ID,也就是用自己的雪花算法在 INSERT 时自动填充主键。这会导致 ShardingSphere 的 key-generate-strategy 完全失效,ID 来源混乱。解决方式是把 MyBatis-Plus 的主键策略改成
auto或input,把主键生成权完全交给 ShardingSphere。
5. 避坑:Spring Boot 整合 ShardingSphere 5.2.1 的五个翻车点
这一章是之前几个项目里沉淀下来的血泪经验,每条都是“现象 → 原因 → 解决”的完整链路,按遇到频率排序。任何一条没处理好,都可能让前面的配置白费。
现象:应用启动直接报NoClassDefFoundError或DataSource相关异常,错误堆栈里能看到javax.sql字样。原因:项目用了 Spring Boot 3.x,而 ShardingSphere 5.2.1 的 starter 基于javax命名空间开发,Spring Boot 3 已经全面迁移到jakarta,两者无法兼容。解决:如果项目是 Spring Boot 2.7.x,直接把 5.2.1 配上即可;如果已经用了 Spring Boot 3.x,不要强行降级 Boot 版本,而是升级 ShardingSphere 到适配 Spring Boot 3 的后续 5.x 版本,或者等整个项目稳定后再统一升级。这个版本匹配问题要在一开始就想清楚,否则后面换版本会牵连大量配置改写。
现象:配置看起来完全正常,启动也不报错,但sql-show打印的 SQL 只有逻辑表名,没有出现t_order_0这样的物理表名,数据实际落在一个库里。原因:配置里把逻辑表名写成了物理表名,比如tables节点下写的是t_order_0而不是t_order;或者actual-data-nodes里的数据源名和datasource.names里定义的不一致,表达式展开后找不到对应数据源,ShardingSphere 只能走默认数据源。解决:检查tables节点下的 key 必须是 SQL 里使用的逻辑表名;核对actual-data-nodes展开后的结果,比如ds$->{0..1}对应 ds0、ds1,如果names里写的是ds-0,表达式就要改成ds-$->{0..1}。这一条看起来低级,但排查成本很高,因为毫无报错提示。
现象:WHERE 条件里没有分片键时,SQL 被广播到所有分片,每个分片都执行一遍,比如查user_id匹配的订单时,四个物理表全被扫了。原因:ShardingSphere 的 standard 路由策略只认sharding-column配置的列,只要 SQL 里没有这个列,它就认为无法确定目标分片,于是把 SQL 发给所有分片节点。解决:业务查询尽量带上分片键;如果是管理后台类查询实在无法带分片键,用 HintManager 在代码里指定分片值,或者接受“广播查询”的性能开销并做好限流。我一般会统计这类 SQL 的占比,超过 10% 就要重新思考分片键选得对不对。
现象:插入数据后,主键出现重复,或者同一批插入的订单 ID 在分片之间毫无规律。原因:MyBatis-Plus 默认的ASSIGN_ID主键策略和 ShardingSphere 的 key-generate-strategy 同时生效,两条雪花算法链路产出的 ID 可能在并发下碰撞,而且 ShardingSphere 的生成器根本没机会执行。解决:在 MyBatis-Plus 全局配置里把id-type改为auto,让 INSERT 语句不携带主键列,由 ShardingSphere 统一生成。如果数据库不支持自增主键,就改成input,手动传入由 ShardingSphere 生成后的 ID。这条坑发生在整合快完成时,最容易让人怀疑是分片算法写错了。
现象:上线后主库 CPU 飙升,从库压力很小;同时连接池频繁报connection is not available超时。原因:两个问题叠加在一起——事务内的读操作被强制路由到主库,导致主库读压力翻倍;分片又把一条 SQL 拆成 N 条物理 SQL,按 4 个分片算,一个请求可能同时占用 4 个连接,连接池很快被打穿。解决:事务边界收紧,只读方法不挂@Transactional;连接池maximum-pool-size按“单条 SQL 最大分片数 × 并发数 + 缓冲”来设置,比如 4 分片、预估 50 并发,连接池需要配置到 200 左右,不能沿用单库时的 20。这两条修正完,主库和连接池的压力会立刻恢复正常。
6. 进阶:用 Hint 兜底缺失分片键的查询,并验证分片均匀性
分片键缺失是常态,尤其是后台运营要按用户手机号、按商品类目查订单的时候。这类查询无法天然携带分片键,最实用的做法是用 HintManager 显式指定分片值。下面这段代码可以把查询路由到指定订单 ID 对应的分片:
public List<Order> queryByCondition(OrderQuery condition) { try (HintManager hintManager = HintManager.getInstance()) { hintManager.addDatabaseShardingValue("t_order", condition.getOrderId()); hintManager.addTableShardingValue("t_order", condition.getOrderId()); return orderMapper.selectByCondition(condition); } }逻辑说明:addDatabaseShardingValue和addTableShardingValue分别为库分片和表分片指定分片值,ShardingSphere 会根据这个值直接计算目标分片,不再要求 WHERE 里出现分片键。注意 try-with-resources 的用法,确保 HintManager 随代码块结束自动清理,否则线程池复用会带着上一次的路由状态。
分片效果好不好,不能只看配置。上线前做一个最简单的均匀性验证:往订单表批量插入 10 万条测试数据,然后在每个物理库上分别统计行数:
SELECT COUNT(*) AS cnt FROM t_order_0; SELECT COUNT(*) AS cnt FROM t_order_1;如果四个表的行数差出一个数量级,先怀疑分片键本身有热点,比如用用户所在城市取模,大城市用户集中必然倾斜;再怀疑算法选型,字符串键用 INLINE 做% 2会集中在某几个哈希值上,换成 HASH_MOD 通常能改善。我自己在早期项目里吃过亏——因为偷懒让不少查询走了 Hint,结果 Hint 调用越加越多,路由上下文被当成全局变量在传,最后花了两个晚上把所有 Hint 全部改成显式带分片键的 SQL,代码清爽了,路由也稳定了。所以 Hint 是急救箱,不是日常口粮。希望帮到你。
本文还有配套的精品资源,点击获取