Spring Boot整合ShardingSphere 5.2.1:分库分表与读写分离实践指南

📅 发布时间:2026/10/20 17:24:14
Spring Boot整合ShardingSphere 5.2.1:分库分表与读写分离实践指南
简介面向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 groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-spring-boot-starter/artifactId version5.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?useSSLfalseserverTimezoneAsia/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?useSSLfalseserverTimezoneAsia/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?useSSLfalse username: root password: root read_ds_0: type: com.zaxxer.hikari.HikariDataSource jdbc-url: jdbc:mysql://192.168.1.21:3306/shop?useSSLfalse username: root password: root read_ds_1: type: com.zaxxer.hikari.HikariDataSource jdbc-url: jdbc:mysql://192.168.1.22:3306/shop?useSSLfalse 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); // 走主库 ListOrderItem 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后下面这条深分页 SQLSELECT * FROM t_order ORDER BY create_time DESC LIMIT 10000, 20;会被改写成类似这样两条实际执行的 SQLSELECT * 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_idShardingSphere 可以直接定位到目标分片每个分片只需要查 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 ListOrder 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 是急救箱不是日常口粮。希望帮到你。本文还有配套的精品资源点击获取