☰
Spring Boot集成ShardingSphere 5.2.1分库分表实战:从配置到踩坑
2026/10/5 15:50:06 网站建设 项目流程

上个月我接手一个订单中心时,单表已经堆到接近1.5亿行,慢查询日志里一半以上都是订单表的。当时团队已经明确要上分库分表,我在方案选型里最终定了 ShardingSphere5.2.1 配合 Spring Boot。这篇文章就是把从选型、依赖引入、配置拆分到实际踩坑的完整过程记录下来,给准备在 Spring Boot 项目里快速上手分库分表的同学做一份可以直接照着实操的参考。

我会默认你已经有基本的 Spring Boot 和 MyBatis 使用经验,对分库分表的概念也有个大概了解。文章里不会把官网文档整篇翻译一遍,而是把“从零搭起来、跑通第一个增删改查、并知道如何验证分片是否生效”这几件事讲清楚。如果你正卡在版本选择、配置不生效、SQL乱路由这几个坎上,这篇应该能帮你省下不少时间。

1. 为什么选 ShardingSphere 5.2.1:从方案选型说起

1.1 在 MyCat、ShardingSphere-JDBC、手写路由中间层之间怎么取舍

分库分表的落地方式无非那么几条路:中间件代理(MyCat/ShardingSphere-Proxy)、应用内嵌入式数据源(ShardingSphere-JDBC)、以及最原始的手写路由逻辑。

多数项目最开始是手写,比如在 Service 层根据订单号取模,动态拼出t_order_0到t_order_7这种表名。这种方式业务代码里到处是"t_order_" + (orderId % 8),写起来爽,维护起来想哭。关联查询、分页、排序全部要自己处理,一旦分片规则调整,波及面是整个业务层。所以手写只适合临时小范围实验,不适合往前飞。

MyCat 这类独立代理的方案我之前也用过,它对应用透明,SQL 进去之后由代理层做路由,团队不用改代码。代价是架构里多了一个必须保证高可用的关键节点,网络链路多一跳,排查问题时要多查一层中间件日志。对没有专职 DBA 团队的中小项目来说,部署和维护成本不算低。

ShardingSphere-JDBC 是嵌入应用进程内的轻量方案,以 jar 包形式引入后,它自己包装了一个 DataSource,业务代码拿到的还是那个熟悉的DataSource,MyBatis、JPA 都能无缝接。路由、改写、归并都在应用内完成,不依赖额外进程。对绝大多数 Spring Boot 单体应用来说,这是最“轻”的上车方式。我在这次选型里最终用的就是它,具体依赖是shardingsphere-jdbc-core-spring-boot-starter的 5.2.1 版本。

1.2 为什么锁定 5.2.1 而不是更旧的 4.x 或更新的 5.4

版本选择上我吃过亏。如果一上来就去搜教程,很容易搜到 ShardingSphere 4.x 的老配置,比如sharding-column: order_id直接写在 table 节点下面,或者是spring.shardingsphere.common这种老段位。照抄基本是启动报错或者规则不生效。5.x 重构了配置模型,分片算法、分布式ID生成器都独立成配置节点,语义清晰很多。

5.2.1 是 5.x 体系里相当稳的一个小版本,官方文档和社区案例密度都高。5.3、5.4 虽然更新,但 starter 的 artifact 有改动,部分内部 API 调整会带来兼容性差异。如果你不是追新特性的场景,5.2.1 足够扛住常规订单、用户、流水类数据的水平拆分。

另外一个关键点是 Spring Boot 版本配套。ShardingSphere 5.2.1 对 Spring Boot 2.x 的支持是完整的,我实测用 2.7.x 系列非常稳。Spring Boot 3.x 是基于 Spring Framework 6 的,5.2.1 当时并没有针对性适配,硬上会碰到循环依赖、代理方式等奇奇怪怪的问题。如果你项目里已经是 Spring Boot 3,那不建议用 5.2.1,需要换到后续支持 Spring Boot 3 的版本。

2. 环境准备与依赖引入:版本搭配是第一道坎

2.1 Spring Boot 2.7.x 与 JDK/驱动版本组合

我这次项目实际上用的是 Spring Boot 2.7.14,JDK 8。这里额外说一句,JDK 8 直到现在依然是很多后端服务的主力运行时,Spring Boot 2.7 对 JDK 8 的支持非常成熟,所以不用为了用新特性把 JDK 升到 17,没必要。

数据库是 MySQL 8.0,驱动用mysql-connector-java8.0.x 版本。需要注意的是,Spring Boot 2.7 的依赖管理可能已经帮你管了这个驱动版本,但为了和 ShardingSphere 5.2.1 的兼容性,我建议显式指定驱动版本,避免因为驱动内部行为差异导致连接初始化异常。

另外一个容易被忽略的点是连接池。ShardingSphere 配置数据源时用的是 HikariCP,对应的类型是com.zaxxer.hikari.HikariDataSource。HikariCP 在 Spring Boot 2.x 里是默认连接池,所以你只要确保项目里没有额外引入其他连接池把 Hikari 冲掉就行。实际排查中我见过有同事引入了 Druid 之后,ShardingSphere 初始化时拿不到正确的数据源类型,报Cannot convert value of type错误。

2.2 Maven 依赖:一个 starter 加一个数据库驱动

核心依赖就两个:

<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.2.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.31</version> </dependency>

如果你项目里已经通过spring-boot-starter-jdbc引用了驱动,mysql-connector-java那块可以省略。想要用 MyBatis 就再引入mybatis-spring-boot-starter2.3.x,这里不单独展开。

这里有个容易踩的依赖冲突点:ShardingSphere 5.2.1 会传递引入较新版本的 Guava 和 Jackson。如果项目里其他地方强制指定了老版本 Guava,比如项目里为了兼容某个老 SDK 把 Guava 压到 18.0,ShardingSphere 内部可能直接 NoSuchMethodError。这种错误不会在启动时就暴露,通常是调用分片算法时才炸。解决方法不是去排除 Guava,而是尽量保持依赖版本统一,或者借助 Maven 的 dependencyManagement 把 ShardingSphere 需要的版本统一调上来。

2.3 老项目改造时要注意的依赖残留

如果是已有项目接分库分表,我最想提醒的是别把旧的 ShardingSphere 4.x 依赖残留着。4.x 和 5.x 的类名大量重叠,但内部 design 完全变了,两个版本的 starter 同时出现在 classpath 里,会导致启动时出现各种BeanDefinitionStoreException或者找不到算法类的诡异报错。

检查手段很简单,本地执行:

mvn dependency:tree -Dincludes=org.apache.shardingsphere

把输出里旧坐标比如sharding-jdbc-spring-boot-starter找出来排除掉。ShardingSphere 从 5.1 开始就已经把坐标改成shardingsphere-jdbc-core-spring-boot-starter,如果你的项目是从老版本升级上来的,这一步不能省。

3. 分片规则设计:建表、分片算法、绑定表与广播表

3.1 演示场景:按订单号取模的订单库

假设业务场景是订单系统,订单量每天几百万,单表扛不住。我设计了两个物理库db_order_0和db_order_1,每个库下面订单表拆成 4 张,t_order_0到t_order_3,这样一共 8 个物理分片。

分片键选订单号order_id,因为它业务上具有全局唯一性,而且几乎所有订单查询都会带它。路由策略是:

  • 分库:order_id % 2,结果为 0 进db_order_0,为 1 进db_order_1
  • 分表:order_id % 4,结果为 0 进t_order_0,为 1 进t_order_1,以此类推

为什么库和表都基于同一个分片键取模?因为这样能让同一笔订单的数据和它的明细表落在同一个数据节点上,后续订单表和订单明细表做关联查询时才不会出现跨库 join。这是个非常关键的设计决策。

我见过其他团队为了平衡数据量,库用user_id分、表用order_id分,结果订单主表和明细表经常跨库,查询只能靠应用层拼装,分布式事务也跟着升级,复杂度直接翻倍。所以对于快速开始的项目,能用同一个分片键就别用两个。

3.2 建表语句:每个库里建 4 张表,表结构必须完全一致

下面给出db_order_0库里订单表的建表语句。注意,表结构里的主键不要用数据库自增,分布式场景下自增 ID 没有任何全局唯一性,后面分布式 ID 章节会细说。

CREATE TABLE `t_order_0` ( `id` bigint NOT NULL COMMENT '全局唯一主键', `order_id` bigint NOT NULL COMMENT '订单号,同时也是分片键', `user_id` bigint DEFAULT NULL COMMENT '用户ID', `order_amount` decimal(12,2) DEFAULT NULL COMMENT '订单金额', `create_time` datetime DEFAULT NULL COMMENT '下单时间', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单分片表0';

然后依次建t_order_1、t_order_2、t_order_3,结构一模一样。再在db_order_1库里也建同样的 4 张表。

订单明细表t_order_item也是同样的套路,每个库 4 张明细表。注意明细表的分片键也要是order_id,这样主表与明细表用同一个键分片,才能配置绑定表。

3.3 application.yml 配置逐项拆解

直接给出我验证过可用的核心配置,再加逐项说明。

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/db_order_0?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: '123456' ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.11:3306/db_order_1?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: '123456' 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: table-inline key-generate-strategy: column: order_id keygen-algorithm-name: snowflake_id t_order_item: actual-data-nodes: ds$->{0..1}.t_order_item_$->{0..3} table-strategy: standard: sharding-column: order_id sharding-algorithm-name: table-inline key-generate-strategy: column: id keygen-algorithm-name: snowflake_id binding-tables: - t_order,t_order_item broadcast-tables: - t_dict sharding-algorithms: database-inline: type: INLINE props: algorithm-expression: ds$->{order_id % 2} table-inline: type: INLINE props: algorithm-expression: t_order_$->{order_id % 4} key-generators: snowflake_id: type: SNOWFLAKE props: worker-id: 1 props: sql-show: true

逐项说几个容易出问题的地方。

actual-data-nodes里的ds$->{0..1}是 ShardingSphere 自己的内联表达式,用于展开枚举范围,结果就是ds0.t_order_0、ds0.t_order_1……全部物理节点。很多初学者写成ds${0..1},这在 Spring Boot 的application.yml里会被 Spring 当作占位符提前解析,导致运行时节点全部失效。5.x 推荐用$->{}就能避免和 Spring 属性占位符冲突。

sharding-algorithms里database-inline和table-inline是两个独立算法配置。这里的INLINE是 Groovy 表达式算法,表达式里出现的order_id是数据库列名,不是 Java 实体字段名。很多人在这里把实体类的驼峰字段写进去,结果发现路由出来的表永远不对。记住,分片表达式里出现的都是物理列名。

key-generators里配置了SNOWFLAKE算法,并指定worker-id。这个 worker-id 在多实例部署时非常重要,每个应用实例的 worker-id 必须不同,否则两个实例生成的雪花 ID 可能重复,进而导致分片数据乱掉。我见过生产环境因为全用默认 worker-id,上线后出现主键冲突的事故。

sql-show: true是开发调试阶段的利器,它会打印逻辑 SQL 和真实路由 SQL,生产环境记得关掉。

3.4 绑定表和广播表:两个容易忽略但很关键的配置

配置里的binding-tables: t_order,t_order_item就是绑定表。含义是这两张表的分片键和分片策略完全相同,ShardingSphere 在遇到两张绑定表的 join 查询时,可以精确路由到同一个数据节点,避免笛卡尔积型的全节点扫描。

举个例子,如果订单表和订单明细表都按order_id分片,那么查询订单和明细 join 时,理论上只需要匹配同库同表的物理分片即可。没有绑定表配置的情况下,ShardingSphere 不知道两张表的分片规则对应关系,会把两个表的所有分片做一次笛卡尔积组合,比如 8×8=64 种组合,只是执行计划大,性能完全不可接受。配了绑定表后,路由组合会收窄到 8 个。

广播表则是所有分片库里都要保存一份相同数据的表,比如字典表、系统配置表。配置了broadcast-tables: t_dict后,插入、更新、删除会同步到所有库,查询就随机取一个节点。这个设计很贴心,但要注意写入频次,写一次放多个库,事务边界会变复杂,所以广播表只放极低写入频率的字典数据。

4. 代码接入:MyBatis 逻辑表 CRUD 的注意事项

4.1 实体与 Mapper:永远写逻辑表名

物理表是t_order_0到t_order_3,但业务代码里写的是逻辑表名t_order,ShardingSphere 会在路由层自动改写 SQL。

实体类就是普通 POJO:

@Data @TableName("t_order") public class OrderDO { private Long id; private Long orderId; private Long userId; private BigDecimal orderAmount; private LocalDateTime createTime; }

@TableName 是 MyBatis-Plus 的注解,如果你用原生 MyBatis,XML 里的表名也写t_order就行。

Mapper 接口我以原生 MyBatis 为例:

@Mapper public interface OrderMapper { @Insert("INSERT INTO t_order (id, order_id, user_id, order_amount, create_time) " + "VALUES (#{id}, #{orderId}, #{userId}, #{orderAmount}, #{createTime})") int insert(OrderDO orderDO); @Select("SELECT * FROM t_order WHERE order_id = #{orderId}") OrderDO selectByOrderId(@Param("orderId") Long orderId); @Select("SELECT * FROM t_order WHERE order_id = #{orderId} AND user_id = #{userId}") OrderDO selectByOrderIdAndUserId(@Param("orderId") Long orderId, @Param("userId") Long userId); }

注意 SQL 里主键字段是显式写入的,因为主键由应用层生成,插库时不会依赖数据库自增。为什么这么做,下一节展开。

4.2 分布式 ID:不要指望 ShardingSphere 自动回填

配置里我加了key-generate-strategy,它的作用是当 INSERT 语句里没有order_id时,ShardingSphere 会调用雪花算法自动生成 ID 并写进去。听起来很省事,但有一个隐含坑:这个由 ShardingSphere 生成的 ID 并不会回填到你的 Java 实体对象里。

假设你在insert(orderDO)之后立刻打印orderDO.getOrderId(),大概率打印出null,但数据库里那行数据已经有一个不同的 ID 了。如果后面业务要拿订单号去发 MQ、查流水,就会对不上。

所以我在实际项目里的建议是:应用层自己生成分布式 ID,插入前就把orderId和主键都 set 好。这样能保证程序里的对象和数据库记录完全一致。

生成 Snowflake ID 的方式有很多,Hutool 的IdUtil.getSnowflakeNextId()就能满足大部分场景,或者直接用 ShardingSphere 底层算法。关键点只有一个:全局唯一,并且同一套订单表体系里的 ID 必须同源生成。

另外千万别给物理表保留AUTO_INCREMENT自增属性后再用 ShardingSphere。逻辑上这是 8 张表,每张表的自增计数都从 1 开始,插入 8 笔订单会出现 8 个id=1的记录。虽然分片键不同不至于物理冲突,但逻辑主键语义完全废掉,任何按 ID 的关联查询都会串。所以我建表时主键列都是普通 bigint,不带自增,主键和分片键都由应用控制。

4.3 插入和查询:分片键必须出现在 SQL 中

路由的前提是 SQL 里有分片键。插入时order_id当然在字段里;查询时如果你只按user_id查,ShardingSphere 无法定位具体节点,只能全库全表扫描。

举一个最常见的问题查询:

SELECT * FROM t_order WHERE user_id = 10086

这条 SQL 走的是广播路由,每个库的每张表都要扫一遍,8 个分片全部响应,最后归并结果。单条数据量小的时候感觉不到,一旦某个user_id的订单量上万,这个查询会直接拖垮数据库。

我处理后端查询接口时,强制要求所有订单查询接口的入参必须包含order_id,至少选项上要支持按order_id精确过滤。前端如果不传订单号只传用户 id,那就走旁路的用户订单索引表,或者用搜索引擎,而不是让分片库全量扫。

如果业务实在绕不开不带分片键的查询,可以借助其他组件维护一个“分片键映射索引”,比如用 Redis 或者 ES 存user_id -> order_id的映射,先查出订单号,再带着订单号走分片查询。这是很朴素但非常有效的方案。

5. 实测中踩过的坑:路由失效、事务失效与配置不生效

5.1 SQL 没带分片键,路由“帮倒忙”的现场

我印象最深的一个生产问题是,报表系统凌晨跑任务,查最近三个月的订单明细,SQL 长这样:

SELECT * FROM t_order WHERE create_time >= '2023-01-01'

分片键order_id完全没出现。ShardingSphere 的处理方式是把这条 SQL 复制到所有 8 个物理分片上去执行,然后再把结果合并。从逻辑结果看没错,但从性能看是一次不折不扣的全表扫描风暴。那天晚上慢查询日志直接把监控打爆了。

所以要提前在团队规范里明确:对分片表的所有查询,SQL 里必须带分片键。不仅是=,IN这种精确条件也可以精确路由。范围条件比如BETWEEN、>、<,INLINE 算法不擅长,也会退化成全路由。如果你的系统里有大量这类范围查询,要么换支持范围分片的算法,要么像上面说的维护映射索引。

5.2 @Transactional 默认管不了跨库事务

ShardingSphere-JDBC 在 Spring 工程里,默认事务类型是本地事务。当@Transactional方法里只操作一个分片时没问题,但它一旦要更新两个不同的物理库,本地事务就失效了,一个库提交成功、另一个库提交失败时,不会自动回滚,数据就处在不一致状态。

这个坑很多从单表单库迁过来的同事都会踩。我当时有一个下单后同时写订单表和扣减库存余额的逻辑,订单在ds0,用户余额在ds1,加了个@Transactional以为万事大吉,结果模拟余额扣减失败时,订单还是落库了。

解决方案有几个层次:

  • 业务设计上尽量让一次操作只落在一个分片内,比如所有关联数据都按同一个分片键组织。
  • 如果必须跨库,需要引入分布式事务。ShardingSphere 5.2.1 支持 XA 事务,需要额外引入shardingsphere-transaction-xa-core,并且在方法上使用@ShardingSphereTransactionType(TransactionType.XA)。
  • 也可以用 Seata 的 AT 模式,但 seata-server 本身需要部署和运维,复杂度更高。

先想清楚业务能不能按分片键收敛,是成本最低也最稳的做法。

5.3 配置属性名差一个词,启动一半就挂

ShardingSphere 的配置键比较长,而且不同版本有差异,抄博客时特别容易踩。

常见错误是把keygen-algorithm-name写成key-generator-name,或者把sharding-algorithm-name写成algorithm-name。这些属性在 5.x 中拼错后,Spring Boot 的宽松绑定不一定能及时给你报错,而是会当作普通配置项忽略,后果就是启动日志显示规则正常,实际跑起来插入时没有 ID 生成,或者分片策略完全没生效。

我的建议是配置写完以后,启动时重点观察 ShardingSphere 的初始化日志。它会打印加载的分片规则,包括Sharding tables load和算法注册信息。如果没看到t_order的节点列表,那大概率就是配置键拼错了。

5.4 排查思路:先看启动日志,再看 Actual SQL

如果你已经出现了路由不对、CRUD 报错等问题,我的排查顺序是固定的:

先用开头的sql-show: true日志确认路由目标。日志里会出现两类 SQL:

  • Logic SQL:业务写的逻辑 SQL,比如SELECT * FROM t_order WHERE order_id = 123
  • Actual SQL:真实路由后的 SQL,比如SELECT * FROM t_order_2 WHERE order_id = 123,前缀可能还会带上ds0这样的数据源标记

如果Actual SQL里的表名还是逻辑表名t_order,说明 ShardingSphere 根本没有接管这条 SQL。这时候去检查 starter 依赖有没有生效,spring.shardingsphere配置有没有被 Spring Boot 读取。

如果表名被改写成了多个分片并且全部执行,说明分片键没有出现在条件中,去补条件。

如果Actual SQL只有一条但表号不对,比如order_id=5应该进t_order_1,结果进t_order_0,大概率是表达式里取模基准错了,检查order_id % 4的算法表达式,以及数据库列的实际类型。列类型是 varchar 而表达式里直接% 4也会出问题,需要用HASH_MOD之类的算法先哈希再取模。

实际操作中,我 90% 的问题靠这两步就能定位。

6. 验证分片是否生效的三种方法

6.1 日志法:观察 Actual SQL 的路由结果

开发阶段最直接的验证方式就是开启sql-show: true。插入一条order_id = 5的数据,理想日志应该是:

  • 逻辑 SQL:INSERT INTO t_order (...) VALUES (...)
  • 实际 SQL:Actual SQL: ds1 ::: INSERT INTO t_order_1 (...) VALUES (...)

因为5 % 2 = 1进ds1,5 % 4 = 1进t_order_1。如果实际路由到ds0或t_order_0,立即排查算法表达式。

查询验证同理:

SELECT * FROM t_order WHERE order_id = 5

路由到ds1.t_order_1就是正确的。多试几个订单号,把 0 到 7 的余数覆盖全,确认 8 个分片都能按预期命中。

6.2 直连数据库查数据分布

日志法能确认单次路由,但没法确认历史数据分布。我会在验证阶段直接连上两个物理库执行:

-- 在 db_order_0 SELECT count(*) FROM t_order_0; SELECT count(*) FROM t_order_1; SELECT count(*) FROM t_order_2; SELECT count(*) FROM t_order_3;

再对比db_order_1里 4 张表的数量。正常插入 N 条数据后,8 张表的数量应该大致均匀,不会出现某一张表空了的情况。

还可以抽查一些订单号,比如取order_id = 10的记录,手动计算10 % 4 = 2,然后到db_order_0的t_order_2里查。如果查到了,路由逻辑和数据落库位置一致。

6.3 用聚合和分页 SQL 验证归并能力

分库分表之后,普通查询容易验证,但聚合和排序会经过归并引擎,也要提前测。

比如:

SELECT user_id, SUM(order_amount) FROM t_order GROUP BY user_id

这条没有分片键的聚合 SQL 会很辛苦,但它能验证 ShardingSphere 的归并是否正常工作。如果结果和单表时代统计一致,说明归并层没问题。性能上的问题另说,至少正确性有保障。

分页查询也要测。跨分片的分页不是简单 LIMIT,而是每个分片各自取出一部分数据再归并排序,深分页时性能衰减很严重。我建议压测一下第 1000 页之后的翻页查询,大概率会发现不能接受的延迟。后续优化方向是改成按order_id游标分页,或者限制只能按时间倒序取前 N 页。

7. 我对这套组合的真实体会

分库分表不是银弹,但它确实是单表数据量到了瓶颈之后一条很务实的路。ShardingSphere 5.2.1 加 Spring Boot 这套组合,对我来说最大的价值是接入成本低、规则可配置、问题可观测,不需要专门搭一套中间件集群就能先把拆分跑起来。

操作上的几个重点再强调一次:分片键要精挑细选,能覆盖绝大多数查询;INSERT 的主键和分片键统一由应用层生成,别依赖数据库自增;所有业务 SQL 强制约束必须带分片键;跨库事务能避免就避免;上线之前先把sql-show打开,全链路确认一遍 Actual SQL。

如果你第一次做分库分表,我建议先拿一个只读报表或低峰期的订单查询接口做试点,不要直接切核心写链路。等路由、归并、分布式 ID 都经过了充分验证,再逐步扩大范围。后面我有时间的话,再单独写写 ShardingSphere 的分布式事务接入和读写分离组合配置,这里也算留个引子。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询