很多做分库分表的朋友,第一反应都是先去找ShardingJDBC的文档,结果被一堆概念和配置项劝退。这个系列第二篇,我直接讲最落地的东西:基于yaml的基本配置。t_order按user_id分库、按order_id分表,从数据源定义到分片算法,一步步说清楚每段配置到底在干什么。这篇适合刚接触ShardingJDBC的Java开发,也适合已经跑通但想系统理清配置结构的人。
先说清楚,本文示例基于ShardingJDBC 5.x版本(具体以5.4.x为准),和4.x的配置结构差异比较大,后文会专门把版本坑列出来。你手里的依赖如果还是4.x,配置路径完全不同,先确认版本再对照操作。
1. 为什么选择yaml配置方式
1.1 yaml vs properties:从可维护性角度选型
ShardingJDBC支持两种配置载体: properties文件和yaml文件。我见过很多老项目用properties写分片规则,十几行还能忍,一旦涉及复合分片、绑定表、分布式ID生成器,properties的扁平结构会让你崩溃。spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-column=user_id,这种长键写到第8层,少一个前缀就全盘不生效。
yaml的优势在于天然支持层级嵌套,分片规则本身就是树形结构,用yaml表达非常直观。而且SpringBoot 2.4之后对yaml的加载机制更友好,配置刷新、profile切换都比properties顺滑。所以新项目我无条件推荐yaml,老项目迁移配置也只是体力活。
还有一个现实原因:ShardingJDBC官方文档、社区帖子、源码注释里的示例代码,绝大多数是yaml格式(ShardingSphere 5.x原生配置就是yaml风格)。跟官方示例保持一致,排查问题的时候对照文档、搜社区更省力。
1.2 配置结构拆解:从整体视角理解五个核心块
一份完整的ShardingJDBC yaml配置,顶层是spring.shardingsphere(注意5.x是这个名字,4.x是spring.shardingsphere没错,但3.x是spring.shardingsphere.datasource开头,写错前缀配置静默失效),下面挂四部分内容:
datasource:数据源集合,定义每个物理库的连接信息。rules:规则集合,分片规则(sharding)、读写分离(readwrite-splitting)、加密(encrypt)都在这下面。props:全局属性,比如是否打印SQL、允许跨库查询开关。mode:运行模式,单机模式用memory(默认),集群模式用cluster,5.x里这个配置很重要但默认值够用。
我推荐的阅读顺序是:先看datasource,理解有多少个库;再看rules.sharding.tables,理解有哪些表要分片、按什么分;然后看algorithms,理解分片算法是怎么定义的;最后看props,打开SQL调试。
这套结构里,最容易混淆的是actual-data-nodes。很多人把它理解成"表名规则",但ds$->{0..1}.t_order_$->{0..1}的完整含义是:数据源ds0和ds1里,各有t_order_0和t_order_1两张物理表,一共4张物理表参与路由。这不是模糊匹配,是精确的节点清单。
2. 核心配置项逐一拆解
2.1 数据源定义:类型、连接参数和默认源的坑
先定义一个最标准的双数据源场景:
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/ds0?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/ds1?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root注意两个细节。第一,names必须和后面每个数据源的key完全一致,多写一个、少写一个,启动时直接报datasource name not found。第二,jdbc-url而不是url,ShardingJDBC要求连真实库时用jdbc-url,因为它要拿这个参数包装自己的DataSource,写url在某些版本里不识别。
我建议至少配置一个default-data-source。虽然分片规则明确下大部分SQL都能命中路由,但总有例外——比如某条SQL只查广播表,或者某个事务里临时跨库操作。不设置默认源,这些SQL直接报Can not find a data source。虽然有人说"默认源就是第一个数据源",但显式配置比隐式猜测可靠:
spring: shardingsphere: datasource: names: ds0, ds1 default-data-source-name: ds0还有一个HikariCP的细节:如果业务量不大,别在yaml里写死maximum-pool-size之类的参数,让连接池用默认值。我第一次就写了minimum-idle: 2,结果并发一上来,连接池要动态扩容,反而增加延迟。小项目就别做这种预优化了。
2.2 分片规则:分片键、策略和算法三者如何配合
分片规则是ShardingJDBC的核心,先看一个完整的表配置:
spring: shardingsphere: 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: db_inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: table_inline key-generate-strategy: column: order_id key-generator-name: snowflake这段配置讲了三件事:这张表落在哪些物理节点(actual-data-nodes);按user_id算库、按order_id算表(两个strategy);order_id由雪花算法生成(key-generate-strategy)。
很多人写分片键时习惯使用userId而不是user_id,注意ShardingJDBC的分片键必须对应数据库表的真实列名。如果你的实体里是userId、表里是user_id,这里一定要写user_id。
database-strategy和table-strategy都用了standard类型,这意味着分片键是单列且操作符只支持=和IN。如果你要按非分片键查询,ShardingJDBC会全路由扫描所有分片,性能问题先不说,至少不会报错——但要小心跨库联合查询子查询的兼容性问题。
2.3 分片算法定义:inline表达式是最常用的入门算法
分片策略引用了算法名字,算法本身在sharding-algorithms节点里定义:
spring: shardingsphere: rules: sharding: sharding-algorithms: db_inline: type: INLINE props: algorithm-expression: ds$->{user_id % 2} table_inline: type: INLINE props: algorithm-expression: t_order_$->{order_id % 2}INLINE算法就是Groovy表达式求值。ds$->{user_id % 2}的意思是:取分片键user_id的值求余2,得到0或1,拼出ds0或ds1。这是最简单的取模分片。初次上手,用取模一定比用HASH_MOD更容易理解,而且user_id如果本身就是数字类型(比如BIGINT),取模结果稳定、无哈希碰撞。
但有几个很重要的边界:
- 表达式里的
user_id是列名,Groovy表达式上下文会注入当前SQL里该列的值,不能写成#{user_id}或:user_id。 - 如果分片键在SQL里没出现,这个算法不会执行,SQL会走全路由。
$->{...}这个写法是ShardingSphere的约定,不是Groovy的一部分,不要随意改成${...}——那会被Spring误认为占位符配置,shardingsphere会直接不加载规则。
调试inline算法的经验是:先在props里临时加一个键值对,例如allow-range-query-with-inline-sharding: true,否则执行BETWEEN或>操作符时,inline算法默认不支持下推,会直接报错。这个配置在生产环境很有用,但要注意全路由的性能风险。
2.4 绑定表、广播表、雪花算法和props属性
分库分表不只是单表拆分,表之间还有关联关系。在分片环境下,JOIN查询如果两张表的分片键一致,但ShardingJDBC不知道这件事,它就只能采用笛卡尔积路由,性能极其糟糕。
用绑定表告诉ShardingJDBC:"主表和子表的分片键是一致的,可以按相同规则路由":
spring: shardingsphere: rules: sharding: binding-tables: - t_order, t_order_itemt_order和t_order_item必须用完全相同的分片键和分片算法,绑定表才有意义。否则,比如t_order按user_id分、t_order_item按user_id分,算法不同,绑定也无法保证一起落库。
广播表的场景更特殊。字典表、配置表这种所有分片库都要完整保留一份的小表,定义为广播表后,ShardingJDBC在所有数据源上执行同样的写操作,读操作取任意一个节点即可:
spring: shardingsphere: rules: sharding: broadcast-tables: - t_dict生成分布式ID,我直接用内置雪花算法:
spring: shardingsphere: rules: sharding: key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1worker-id在多实例部署时一定要配。分布式环境下两个实例如果都用默认worker-id,极端情况下会生成相同ID,虽然概率非常低,但一旦主键冲突,排查成本远高于写一行配置。我用固定worker-id而不是自动生成,是因为测试环境可以确定性地复现问题。
props里最常用的两个参数:
spring: shardingsphere: props: sql-show: true sql-simple: falsesql-show: true能把实际路由到哪个库哪张表的SQL打印出来,排查问题的时候第一时间打开它。生产环境必须关掉,日志量太大了。sql-simple控制是否打印简单格式的SQL,方便日志系统解析,但默认关就行。
3. 一个可跑通的最小分库分表示例
3.1 依赖和环境准备
我用的依赖版本组合:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.4.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>这里有个大坑:如果你的项目是SpringBoot 3.x,而依赖用的ShardingJDBC版本还是5.2或更低,启动时会因为javax.sql包和jakarta包冲突直接报错。SpringBoot 3.x必须配ShardingJDBC 5.4.0以上。我用SpringBoot 2.7.18配5.4.1,是官方测试相对稳定的组合。
数据库层面,准备两个MySQL实例(或同一实例的两个库):ds0和ds1。每个库里手工建两张物理表:t_order_0、t_order_1、t_order_item_0、t_order_item_1。ShardingJDBC不会自动建表,这点一定记清楚。
3.2 建表脚本和基础实体
建表脚本:
CREATE TABLE `t_order_0` ( `order_id` bigint(20) NOT NULL, `user_id` bigint(20) DEFAULT NULL, `status` varchar(32) DEFAULT NULL, PRIMARY KEY (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;我的习惯是每张分片表先建壳表,跑通后自动刷表结构再调整索引。分片键user_id一定要建普通索引,否则查主表时全表扫描。
实体和Mapper用最直接的MyBatis写法:
public class Order { private Long orderId; private Long userId; private String status; }@Mapper public interface OrderMapper { @Insert("INSERT INTO t_order(order_id, user_id, status) VALUES(#{orderId}, #{userId}, #{status})") int insert(Order order); @Select("SELECT * FROM t_order WHERE user_id = #{userId} AND order_id = #{orderId}") Order selectById(@Param("userId") Long userId, @Param("orderId") Long orderId); }这里注意SQL里写的表名是t_order,不是t_order_0或者t_order_1。ShardingJDBC在SQL执行时,会根据分片规则把逻辑表名改写成物理表名。如果你在SQL里写了物理表名,分片直接失效。
实体里插入时不需要手动set orderId,让ShardingJDBC的雪花生成器赋值。但MyBatis的@Insert默认不返回自增主键,需要额外配置useGeneratedKeys,或者在Service层插入前主动调用ID生成器,我图省事就等ShardingJDBC在拦截器层面统一生成,接口只接收userId和status。
3.3 完整yaml配置清单
把前面所有片段汇总成一份可直接复制的配置:
spring: shardingsphere: datasource: names: ds0, ds1 default-data-source-name: ds0 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/ds0?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/ds1?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root 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: db_inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: table_inline key-generate-strategy: column: order_id key-generator-name: snowflake binding-tables: - t_order, t_order_item broadcast-tables: - t_dict sharding-algorithms: db_inline: type: INLINE props: algorithm-expression: ds$->{user_id % 2} table_inline: type: INLINE props: algorithm-expression: t_order_$->{order_id % 2} key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1 props: sql-show: true这份配置里actual-data-nodes里的$->{0..1}是ShardingSphere内建语法,不是Spring表达式,所以不会被外层Spring解析。
如果这两个库不在同一实例,跨库的广播表写入会要求binlog开启依赖,建议先用同一实例两个库做demo,省去跨库事务的配置成本。这个选择是为了快速跑通,生产再上distributed transaction。
3.4 验证配置是否生效
启动SpringBoot应用后,观察控制台日志。如果看到类似:
[INFO] ShardingSphere: ShardingSphere 5.4.1 ... [INFO] ShardingSphere: MetaData reload ... [INFO] ShardingSphere: Loading ...说明ShardingJDBC已经加载成功。执行一个Insert操作,开启sql-show后日志会输出:
Actual SQL: ds0 ::: INSERT INTO t_order_1(order_id, user_id, status) VALUES (..., ..., ...)这行输出说明:user_id求余后路由到了ds0,order_id求余后路由到了t_order_1。路由逻辑完全符合配置,配置生效。
先用一个简单的查询验证路由正确性:
Order order = orderMapper.selectById(3L, 12345L);如果只传user_id或只传order_id,配置的分片键无法获取到完整值,就会触发全路由扫描,日志里出现ds0和ds1两个数据源都执行了SQL。这在数据量小的时候无感,但生产环境要尽量避免。
4. 常见配置问题与排查实录
4.1 yaml配置未生效,日志中看不到Sharding相关信息
遇到这个问题的,第一反应就是看依赖版本和配置前缀。
如果SpringBoot正常启动、不报错,但日志里完全没有ShardingSphere的加载信息,先检查pom.xml是否真的引入了shardingsphere-jdbc-core-spring-boot-starter。我遇到一次实际项目里同时引入了sharding-jdbc-spring-boot-starter(4.x的旧包名)和shardingsphere-jdbc-core-spring-boot-starter(5.x新包名),两个包冲突后,规则全部失效,启动还不报错——这个问题极其隐蔽。
还有一种情况:yml文件用了Tab缩进。yaml文件严格禁止Tab缩进,一个Tab出现在行首,解析器会静默忽略后面的内容,配置看起来在但实际未加载。用IDE的yaml插件格式化一下,能解决好多次"配置不生效"的玄学问题。
再仔细检查前缀。5.x版配置前缀一定是spring.shardingsphere,4.x版里用的是spring.shardingsphere,但内部结构不同,如果从网上抄了4.x的配置塞到5.x里,只有一部分配置被识别,很多规则静默被忽略。一般建议把SQL日志打开,看是否有关键字缺失。
排查步骤可以固定下来:先看启动日志有没有ShardingSphere Banner;再看DataSource是否被代理;最后看执行SQL时日志会不会打印Actual SQL。三步走完,大部分配置问题都浮出水面。
4.2 数据源循环依赖或初始化失败
The dependencies of some of the beans in the application context form a cycle,这个报错在ShardingJDBC集成旧版本时很常见。
报错原因是ShardingJDBC 5.x会生成一个ShardingSphereDataSource,它包装了原始数据源,而Spring容器中同时存在原始数据源和代理数据源,导致依赖注入循环。解决方案除了升级到推荐版本(5.4.x已经修复大部分场景),还可以在配置里显式指定使用哪个数据源:
spring: shardingsphere: mode: type: Memory内存模式下,ShardingJDBC不依赖外部存储元数据,全部元数据在启动时从真实库读取后放内存,一般不产生循环依赖。如果还报错,检查是否有自定义的DataSource配置类,比如在@Configuration里手动new了一个DataSource,这会把ShardingJDBC的代理数据源和原生数据源搞混。
另外,如果你的数据源用了Druid,记得核对Druid版本和ShardingJDBC兼容列表。Druid 1.2.6以下的版本和ShardingJDBC 5.x有兼容问题,表现是连接池初始化成功后校验失败,报错信息像极了数据库连接配置错误,实际是Druid内部Filter和ShardingSphere冲突。换HikariCP能避免90%这种问题。
4.3 inline表达式不生效,SQL执行报错找不到表
inline表达式是运行时Groovy求值,写法上有一点疏漏就会让ShardingJDBC路由失败。
最容易犯的错误是:
algorithm-expression: ds${user_id % 2}把$->{...}写成了${...}。这种写法会被属性解析器先处理,可能直接替换为空字符串,最终表达式变成ds,路由全部到ds这个不存在的库。
还有分片键类型不匹配:user_id在数据库是VARCHAR但存储的是数字字符串,取模时Groovy会尝试转Number,但某些版本的表达式引擎对字符串取模会抛异常。我的经验是分片键在业务设计上尽量用BIGINT或INT,不要用字符串。
如果执行查询时遇到Table 'ds0.t_order' doesn't exist,说明WHERE条件里没有分片键,ShardingJDBC回退到全路由,但SQL里又写了t_order这个逻辑表名,某些条件下它没法正确改写。解决思路是确保查询条件必带分片键,这是使用ShardingJDBC的原则。
4.4 分页查询LIMIT被限制为5000条,够用吗
ShardingJDBC默认在分页查询时会做内存合并,合并前会限制LIMIT的最大值。官方默认是max-connections-size-per-query控制连接数,但很多人踩的是另一个坑:
查询LIMIT 100000, 20这种深分页,ShardingJDBC会把每个分片都查一遍然后合并,性能差且容易被限制。我建议的第一步是在props里调大max-connections-size-per-query:
spring: shardingsphere: props: sql-show: true max-connections-size-per-query: 8这个参数控制每个查询对单个分片的最大连接数,调大能提升并行度,但不要无脑调。连接数上限不去控制,分片查询时会占满连接池,业务高峰时反而拖垮其他请求。
深分页优化更要靠业务手段:用上次查询的最后一条order_id作为游标条件,加一个order_id > #{lastId} ORDER BY order_id LIMIT 20,这比OFFSET深分页快几个数量级。分库分表场景下,OFFSET深分页基本是无解的,只能业务规避。
4.5 版本迁移时常见的配置错位
很多团队从4.x升级5.x,最容易踩的错位点:
| 配置项 | 4.x写法 | 5.x写法 |
|---|---|---|
| 数据源key | datasource.ds0.url | datasource.ds0.jdbc-url |
| 表规则 | tables.t_order.actualDataNodes | tables.t_order.actual-data-nodes |
| 分片算法位置 | sharding.defaultDatabaseStrategy | rules.sharding.sharding-algorithms |
| 广播表 | broadcastTables | rules.sharding.broadcast-tables |
| 打印SQL | props.sql.show | props.sql-show |
看着只是命名习惯变化,但5.x把策略和算法拆成了独立定义,4.x里一个defaultDatabaseStrategy搞定的事,5.x必须先定义sharding-algorithms再用sharding-algorithm-name引用。如果只改前缀不重构结构,配置大概率不生效。
我的建议是升级时做一次配置重写,不要做文本级迁移。把表路由、算法、绑定关系列个清单,照着5.x的结构重新组织,比人肉改键值命名要稳得多。
5. 从yaml配置到分片逻辑的完整认知收口
配置写到这里,你自己应该能回答这几个问题了:我的逻辑表对应哪些物理节点;我的分片键是什么,算法怎么定义;全路由和精准路由的区别是什么;绑定表和广播表解决了什么问题。这个认知比背下来配置项更重要。
从我实战的感受来说,yaml配置本身并不难,难的是对每个配置背后的路由逻辑有清晰的感知。刚上手时不要追求配置覆盖所有场景,先跑通最小案例,再去处理绑定表、广播表和分布式ID。分片键选错、表达式写错、版本迁移错位,这三大坑我都踩过,每次都是靠打开sql-show看实际路由到的SQL一步步定位的。
下一篇我会讲读写分离在ShardingJDBC里怎么配,以及分片和读写分离叠加时,数据源命名、负载均衡算法怎么处理。这个组合场景也是网上资料最容易模糊的地方。先用好这篇的yaml配置,把一条SQL从发起到路由到物理库的完整链路跑通,后面所有高可用策略都有基础了。