☰
ShardingSphere-JDBC 5.5.0 + Spring Boot 3 分库分表配置实战与避坑指南
2026/10/10 10:02:35 网站建设 项目流程

1. 为什么是 ShardingSphere-JDBC:单表瓶颈与选型逻辑

1.1 单库单表在什么阶段真的需要分片

先说结论:不是所有项目第一天上 ShardingSphere-JDBC,我这边是在单表数据量接近千万、业务侧频繁出现慢查询之后才动手做的迁移。这段时间最典型的几个信号是:连接数常年告警、某些范围查询即便走索引也要几百毫秒、大促期间主库 IO 明显打满、单表索引再怎么加都救不回来。这时候你再继续把资源砸在"单库单表"这条路上,收益已经很低。

很多人会误以为分库分表是数据量到千万才做的事,其实更准确的判断标准是数据库成为性能瓶颈的时刻。如果你的业务只差几个慢查询,先优化索引、做读写分离、加缓存就能撑很长时间。我自己判断是否需要分片,核心看三条:单表数据量增速是否不可控、单库连接是否持续逼近上限、是否有某些业务必须全表扫描且逻辑上无法避免。

我这次做的是订单模块,订单表天然适合分片,因为 buyer_id(用户 ID)是很稳定的分片键,按用户维度切分之后,同一个用户的所有订单都会落到同一个库的同一张分表上,后续 join 明细表也方便。

1.2 JDBC 模式、Proxy 模式,我为什么选了 JDBC

ShardingSphere 有两个主要形态:ShardingSphere-JDBC 是轻量级的 JDBC 驱动模式,在应用进程内完成分片路由;ShardingSphere-Proxy 则是一个独立部署的中间件服务,对外模拟 MySQL 协议,业务方通过普通连接方式访问。

我用一个表格把两者的核心差异列出来,这也是我当时做决策的最主要依据:

对比项ShardingSphere-JDBCShardingSphere-Proxy
部署形态打进应用进程,无独立中间件节点独立部署服务,业务方网络连接
应用侵入低,只替换数据源和驱动非常低,连接方式都不用改
性能开销应用内做 SQL 解析与路由,无网络跳转多一层网络转发,压测下有一定损耗
多语言支持仅 Java 生态更顺滑语言无关,任何 MySQL 客户端都可以连
运维成本无额外组件,升级跟随应用需要单独维护 Proxy 集群与配置中心

我当时选择 JDBC 模式的核心原因有两个:一是我们整个技术栈就是 Java + Spring Boot,JDBC 模式不需要额外部署一套 Proxy,运维上省事很多;二是 JDBC 模式的路由在应用内存里完成,少了中间层网络开销,对订单这种写多读多的场景更友好。

顺便说下,网上有些人拿 ShardingSphere 和 MyCat 对比,早期的 MyCat 在纯分片场景确实很常见,但 SQL 兼容性、权限支持和社区活跃度这些年明显不如 ShardingSphere。如果不是已经在 MyCat 体系里重度投入,新项目从 ShardingSphere 起步是更稳妥的选择。

1.3 5.x 相对 4.x 的配置变化,决定你照着谁抄

这次做基础配置之前,我在网上搜到大量 ShardingSphere 4.x 的教程,照着抄基本都会翻车。5.x 之后,配置体系有一个非常明顯的变化:配置前缀和规则层级完全不一样。

4.x 是这么写的:

spring: shardingsphere: sharding: tables: t_order: actual-data-nodes: ds0.t_order_0

5.x 把tables放进了rules.sharding.tables下面:

spring: shardingsphere: rules: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}

这种变化直接导致:你在 4.x 时代的百度结果里复制过来的配置,在 5.5.0 下大概率启动失败,报错信息又长又难懂。所以这篇实战笔记里所有配置我都是按 5.x 的规则结构写的,你如果之前只接触过 4.x,请先忘掉旧结构,以这里的配置为准。

2. 5.5.0 + Spring Boot 3 的依赖落地与工程初始化

2.1 版本基线:JDK 17 + Spring Boot 3.x

ShardingSphere-JDBC 5.5.0 对应的 Java 生态基线比较明确:JDK 至少要 17,Spring Boot 建议使用 3.2 或更高版本。我本地环境是 JDK 17 + Spring Boot 3.3.x + MySQL 8,整个组合跑下来没有版本层面的兼容问题。

如果你还在用 Spring Boot 2.x,那么不需要硬上 5.5.0,它对应的是 4.x 到 5.x 过渡期的老 API。Spring Boot 3 下使用 ShardingSphere-JDBC 的 starter,核心依赖就一个:

<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-spring-boot-starter</artifactId> <version>5.5.0</version> </dependency>

这里有个非常坑的点:网上很多文章还在用shardingsphere-jdbc-core-spring-boot-starter这个坐标,它是 5.1.x 及更早版本里的命名。5.5.0 时代官方推荐的坐标是shardingsphere-jdbc-spring-boot-starter,如果你按老坐标引,Maven 可能给你解析到一个版本依赖完全不一致的包,启动时整出各种莫名其妙的NoClassDefFoundError。所以依赖这块建议直接去官方 Maven 仓库搜 5.5.0 对应 artifacts,别盲抄旧博客。

2.2 Spring Boot 自动装配的影响

引入 starter 后,ShardingSphere 会自动接管 Spring 的数据源装配。它会把你在spring.shardingsphere.datasource下定义的多个物理数据源包装成一个逻辑数据源,然后注入到 Spring 容器中。对你来说,原来@Autowired一个DataSource的地方不用改,MyBatis、JdbcTemplate、Spring Data JPA 这些底层框架拿到的就是 ShardingSphere 的逻辑数据源,业务代码对分片完全无感知。

这里要提醒一点:项目里不要同时配置spring.datasource.*和spring.shardingsphere.datasource.*。ShardingSphere 的 starter 会创建一个优先级很高的逻辑数据源 Bean,如果业务里还有其他独立数据源配置,Spring 容器里会出现多个 DataSource,事务管理器或者某些自动装配逻辑会拿错连接,轻则日志一堆警告,重则直接路由失败。我进这个坑的时候,表象是"某个自定义 Mapper 突然连不上库了",排查了半天才发现是配置重叠导致的 Bean 冲突。

2.3 物理库表初始化:先手动建好两个库和四张表

ShardingSphere-JDBC 只是一个数据访问层的分片中间件,它不会在 MySQL 里自动建库建表。分片规则里的actual-data-nodes只负责描述"逻辑表 t_order 对应哪些真实表",这些真实表必须预先存在,否则第一次路由过去就会报Table 'xxx.t_order_0' doesn't exist。

我这次的演示场景是两个物理库、每库两张订单分表:

CREATE DATABASE IF NOT EXISTS sharding_demo_ds0 DEFAULT CHARSET utf8mb4; CREATE DATABASE IF NOT EXISTS sharding_demo_ds1 DEFAULT CHARSET utf8mb4; CREATE TABLE IF NOT EXISTS sharding_demo_ds0.t_order_0 ( order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, order_amount DECIMAL(10,2), order_status TINYINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- sharding_demo_ds0.t_order_1、sharding_demo_ds1.t_order_0、sharding_demo_ds1.t_order_1 -- 建表语句与上面一致,只是库名和表名不同。

注意主键我用的是BIGINT,因为后面配置分布式 ID 时默认用雪花算法,生成的 ID 是 64 位长整型,如果用 INT 会直接溢出。

DDL 这块在启动验证前搞定,不要等到代码跑起来才想起来初始化。

3. 配置逐字段拆解:数据源、分片规则与分布式 ID

3.1 datasource 配置:Hikari 连接池与 jdbc-url

ShardingSphere 5.5.0 在 Spring Boot 里的配置入口是spring.shardingsphere.datasource。多个物理库用names声明,然后每个库独立配置。下面是完整 YAML,我逐个字段注释了用途:

spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/sharding_demo_ds0?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 maximum-pool-size: 20 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/sharding_demo_ds1?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 maximum-pool-size: 20

有两个细节值得说明。第一,type这里我直接用了 HikariCP,因为 Spring Boot 3 默认连接池就是 Hikari,没必要换其他类型。第二,注意是jdbc-url而不是url。ShardingSphere 的 datasource 定义不是 Spring Boot 原生的spring.datasource配置,它要求每个数据源实例有自己的连接池属性字段,用jdbc-url才对。有些博客写url也能跑,那是运气好,严格模式下绑定会失败。

3.2 数据节点表达式:$->{0..1}到底怎么读

分片规则中最容易让新手犯迷糊的是actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}。

这个表达式拆开看并不复杂。ds和t_order_是字面前缀,中间的$->{...}是 ShardingSphere 的行内表达式语法,用来生成 0 到 1 的枚举范围。所以ds$->{0..1}会被展开成ds0和ds1,t_order_$->{0..1}展开成t_order_0和t_order_1,两个范围组合起来就是四张真实表:ds0.t_order_0、ds0.t_order_1、ds1.t_order_0、ds1.t_order_1。

这里有个 YAML 解析层面的坑很容易踩:$->{0..1}里的花括号在 YAML 里会被某些解析器当作 flow mapping 的开始,导致整个配置绑定失败。解决办法很简单——把这个值用单引号包裹起来:

actual-data-nodes: 'ds$->{0..1}.t_order_$->{0..1}'

我第一次没加引号,启动时 Spring Boot 直接报Failed to bind properties一长串,花了半小时才定位到是表达式没加引号的问题。

3.3 分片策略与分片算法:库策略和表策略必须分开配

5.x 的分片规则把"策略"和"算法"分层设计。策略描述:按哪个列分片,用哪个算法;算法描述:具体怎么计算路由目标。

以订单表为例,我的设计是:

  • 库策略:按user_id取模 2,决定落到 ds0 还是 ds1
  • 表策略:按order_id取模 2,决定落到 t_order_0 还是 t_order_1

对应配置如下:

rules: sharding: tables: t_order: actual-data-nodes: 'ds$->{0..1}.t_order_$->{0..1}' database-strategy: standard: sharding-column: user_id sharding-algorithm-name: database_inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: table_inline sharding-algorithms: database_inline: type: INLINE props: algorithm-expression: 'ds$->{user_id % 2}' table_inline: type: INLINE props: algorithm-expression: 't_order_$->{order_id % 2}'

为什么库和表要分开配置?因为分片键可能不一样。订单表按order_id分表天经地义,但是同一个买家的订单如果散落在多个库,后续按买家维度做跨库查询就非常麻烦,所以我额外用user_id做库路由,让同一个用户的订单尽量落在同一个库里,这样后续订单详情、订单明细 join 都不会跨库。

INLINE算法是 5.x 最常用的分片算法,本质就是一个 Groovy 表达式求值。表达式里%是取模运算,计算结果是 0 或 1,刚好对应 ds0/ds1、t_order_0/t_order_1。这里同样要加单引号,ds$->{user_id % 2}里的花括号和空格在无引号状态下可能被 YAML 误解析。

还需要特别提一下,INLINE 定位是单值路由算法,适合等值查询。如果你业务里大量使用BETWEEN、IN这种范围查询,INLINE 的应对能力比较有限,可能出现退化为全节点扫描的情况。正式设计分片规则时,建议针对范围查询业务单独配置标准算法或者自定义算法,基础配置阶段先知道这个边界就行。

3.4 key-generate-strategy:雪花算法与自动填充

分表之后,order_id作为表分片键,必须保证全局唯一,不能依赖数据库自增主键,因为两个库两张表各自自增,必然冲突。ShardingSphere 的解决方案是为逻辑表配置分布式 ID 生成器,默认提供雪花算法(SNOWFLAKE)。

配置如下:

tables: t_order: key-generate-strategy: column: order_id key-generator-name: snowflake key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1

这里有一个很多人第一次用会困惑的点:插入数据时,业务代码里到底要不要给orderId赋值?

我的经验是:不需要,而且尽量不要手动赋值。ShardingSphere 在执行 INSERT 时发现 SQL 列清单里没有order_id,但逻辑表配置了key-generate-strategy,它会在 SQL 解析和改写阶段自动生成雪花 ID,把这个值填充进 INSERT 语句,然后用这个生成的 ID 参与表路由。你在 Java 代码里把orderId设成 null,配合 MyBatis 的useGeneratedKeys,插入完成后还能把生成的主键回填到实体上,整体链路是通的。

不过要注意,雪花 ID 是大整数,如果业务上还有地方依赖顺序递增或者需要短 ID,雪花 ID 不一定适合,那就需要自己实现一个自定义 ID 生成器,或者用 UUID 但把分片键改成其他字段。基础配置先别整太复杂,雪花 ID 够用。

3.5 广播表配置:字典表这类数据怎么处理

分片之后还有个很容易忽略的场景:字典表、配置表这种数据量很小、几乎不会增长、但几乎所有业务表都要关联查询的表,怎么放?

方案是广播表。把t_dict配置到广播表里,ShardingSphere 会让这张表的完整数据在 ds0 和 ds1 两个库各存一份,写入时同步写所有库,查询时从当前路由到的库直接读,避免跨库 join:

broadcast-tables: - t_dict

基础配置阶段,我建议凡是那种"哪里都要查、数据量很小、更新不频繁"的表都考虑设置成广播表。否则一旦业务开始在分片表上 join 字典表,而字典表没同步到所有库,你会在某个分片库上看到报错Table 't_dict' doesn't exist。

3.6 绑定表配置:防笛卡尔积的订单与订单明细

如果只是单表分片,绑定表可以不着急配。但订单模块往往还有订单明细表t_order_item,它跟订单表通常是一对多关系,而且按同样的规则分片。如果不配置绑定表,ShardingSphere 对两张分片表做 join 时,会默认做笛卡尔积路由,比如查询语句可能同时去查询ds0.t_order_0joinds0.t_order_item_1这种明明不存在关联数据的组合,结果慢且错。

配置绑定表让两张表按同样的分片键保持一致的路由结果:

binding-tables: - t_order, t_order_item

注意表名之间的逗号,配置项是一个字符串数组,但这里实际上是逗号分隔的列表写法。绑定表要求两张表的分片键类型、分片算法完全一致,否则路由结果对不齐,配置了也起不到防笛卡尔积的效果。

4. 跑通一条真实 SQL:启动、插入、查询的路由验证

4.1 开启 sql-show,确认启动过程没有报错

配置全部写完后,第一步不是写业务代码,而是把 ShardingSphere 的 SQL 执行日志打开,方便看到路由过程:

props: sql-show: true

加完这个配置后启动 Spring Boot,如果配置没问题,启动日志里能看到 ShardingSphere 的数据源初始化、分片规则加载、广播表注册这些信息。但我要提醒你:启动成功不代表两个物理库都能连上。HikariCP 默认是懒加载连接,ShardingSphere 启动时只是定义连接池,并不会立刻发起网络连接,所以第一个 SQL 执行时可能才暴露库连不上、表不存在的问题。

所以我的验证习惯是:启动通过后,先写一个最简单的接口,强制它走一次 SQL,确认链路真正是通的,再继续后面的开发。

4.2 INSERT 数据:观察 order_id 自动生成与路由

这个阶段我用 MyBatis 做了一个最简单的 OrderMapper,测试方法里循环插入 10 条订单,user_id 从 1 到 10:

@SpringBootTest public class OrderMapperTest { @Autowired private OrderMapper orderMapper; @Test void testInsert() { for (long userId = 1; userId <= 10; userId++) { Order order = new Order(); order.setUserId(userId); order.setOrderAmount(new BigDecimal("99.50")); order.setOrderStatus(1); // orderId 不手动设置,留给 ShardingSphere 生成 orderMapper.insert(order); } } }

SQL 日志开启后,插入时能看到类似下面的输出:

Logic SQL: insert into t_order (user_id, order_amount, order_status) values (?, ?, ?) Actual SQL: ds1 ::: insert into t_order_1 (user_id, order_amount, order_status, order_id) values (?, ?, ?, ?) ::: [3, 99.50, 1, 8901234567...]

这行日志是最直观的验证:Logic SQL是你业务里写的逻辑表 SQL,Actual SQL是 ShardingSphere 改写后真正在物理库执行的 SQL,它自动追加了order_id列并填入了雪花 ID。同时ds1这个前缀说明库路由生效,user_id 为 3 的订单被路由到了 ds1,再配合t_order_1说明表路由也生效了。

如果你发现所有 INSERT 都只落在同一张表,先检查分片表达式的取模逻辑和值域是否匹配,比如user_id % 2的结果是 0 还是 1,是否跟你预期的库编号一致。

4.3 SELECT 查询:逻辑表到真实表的归并

查询验证我用的是 SQL:

@Test void testSelectByUserId() { List<Order> orders = orderMapper.selectByUserId(3L); orders.forEach(o -> System.out.println(o.getOrderId())); }

对应日志:

Logic SQL: select * from t_order where user_id = ? Actual SQL: ds1 ::: select * from t_order_1 where user_id = ? ::: [3] Actual SQL: ds1 ::: select * from t_order_0 where user_id = ? ::: [3]

这里有两个细节值得注意。

第一,按user_id查询时,因为库策略已经路由到 ds1,ShardingSphere 只需要在 ds1 的两张表里分别执行查询再归并结果,不会去 ds0 白白查一遍。

第二,如果你没有指定分片键,比如直接select * from t_order,ShardingSphere 会把所有真实表全部查一遍,日志里会出现四条 Actual SQL。这种全节点查询在数据量不大的演示环境没问题,但生产环境要尽量避免无分片键的查询,它是分片方案里最典型的性能杀手。

验证完插入和查询,基础配置这块就算真正跑通了。后面才是把项目往生产形态推进时容易踩进去的深坑。

5. 真正容易踩的五个坑

5.1 分片表不会自动建,JPA 的 ddl-auto 也别指望

前面说过 ShardingSphere 不负责建表,但实际项目中经常有人漏掉初始化脚本。更隐蔽的问题是:如果你用了 Spring Data JPA,并把ddl-auto配成update,JPA 在启动时会尝试用逻辑表名去建表,它根本不认识t_order_0、t_order_1,更不会帮你把分表全部建出来,结果就是启动时一堆建表语句报错。

我的做法是分库分表环境的表结构一律用 Flyway 或者手工 SQL 管理,每张真实分表都显式建好,绝不依赖 ORM 的建表能力。这一点在团队协作时尤其重要,新人入职如果不知道这个边界,往往会在"为什么我的表没建出来"上卡半天。

5.2 YAML 引号与旧版配置前缀的双重陷阱

这个坑我在第三节里分散提过,但值得单独汇总一下。

一个是表达式必须加引号。我见过同事把actual-data-nodes、algorithm-expression的值原样裸写,启动报mapping values are not allowed in this context,几乎每次都会栽一次。

另一个是 5.x 与 4.x 的结构前缀差异。网上搜到的大批老教程,配置结构是spring.shardingsphere.sharding.tables,而 5.x 是spring.shardingsphere.rules.sharding.tables。你把老配置贴进来,很多字段直接失效,ShardingSphere 可能安静地走默认配置,也可能启动时就报绑定失败。判断一个教程是不是 5.x 的,最快的方法就是看它有没有rules:这一层。没有这个层级,直接劝退。

5.3 跨库 join 不会自动发生,设计分片键要想清楚

ShardingSphere-JDBC 并不是万能的,它支持在分片表之间做 join,但有个大前提:join 的表必须能路由到同一个数据源,路由不到就报错或者产生笛卡尔积。

我这次设计订单表时把库策略设为user_id取模,就是为了保证同一个用户的订单表、订单明细表落在同一个库。如果你把库策略设为order_id,表策略也设为order_id,那么关联t_order和t_order_item时,如果两者的 order_id 一致,理论上是能路由到同一库的,但一旦查询加了非分片键维度的关联条件,路由就乱了。

这是分片设计里最考验经验的部分。做基础配置时可以不去深究所有边界,但心里要清楚:先分片,再关联,永远要确认关联键和分片键能对得上。

5.4 @Transactional 不是分布式事务,别拿它当跨库原子性保障

ShardingSphere-JDBC 默认的事务模式是本地事务。什么叫本地事务?就是每个物理数据源内部的事务。如果你的业务在一个事务里同时更新了 ds0 和 ds1 的数据,@Transactional 只能保证每个库内部各自的原子性,两个库之间的写入是没有全局原子性的。A 库提交成功,B 库提交失败,不会自动回滚 A 库。

5.x 要真正解决跨库事务,方案是接入 Seata 或者采用 XA 分布式事务。基础配置阶段通常不会碰到这种场景,但迟早会。我的建议是:在设计阶段就避免单事务跨库,把涉及多个库的写操作拆成独立事务,通过消息表、本地消息表这类方式做最终一致性,而不是指望一个 @Transactional 包住天下。

5.5 分页插件、逻辑删除与 ShardingSphere 一起用要验一下

MyBatis-Plus 的分页插件PaginationInnerInterceptor跟 ShardingSphere 一起用,多数情况下是兼容的,但你不能默认它一定没问题。ShardingSphere 对分页查询的 LIMIT 处理有自己的归并逻辑,而 MyBatis-Plus 的分页插件也会改写 SQL,两者在复杂分页语句上偶尔会互相干扰,出现分页数据不准、count 查询异常之类的现象。

我的做法是:涉及分片表的分页查询,先关掉 MyBatis-Plus 的分页插件,直接用 ShardingSphere 原生的 LIMIT 支持来跑,等确认查询结果正确,再决定要不要打开插件。同样,逻辑删除字段是 MyBatis-Plus 的常见功能,它在 SQL 改写阶段追加的deleted = 0条件,对 ShardingSphere 来说只是普通的 WHERE 条件,一般没问题,但插入和更新时如果涉及逻辑删除字段的默认值,还是要实际执行一遍看看结果。

我把这段时间实战下来最有感触的一点放在最后说吧。ShardingSphere-JDBC 5.5.0 的基础配置,本质上就是三件事:建好物理表、配好数据源、写对分片表达式。前两件事按部就班就能完成,第三件事才真正决定你后续的业务能否平安跑起来。我自己的建议是,第一次上手先不要追求复杂算法,用最朴素的取模路由跑通一条插入、一条查询,亲眼看到Actual SQL打印出正确的目标表和目标库,再逐步引入广播表、绑定表、分布式事务这些高阶能力。基础链路通了,后面所有问题都有清晰的排查起点;基础链路不通,任何高级配置都是在给下一次故障埋单。

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

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

立即咨询