去年给公司的订单中心做数据拆分时,我第一次完整地把 ShardingSphere-JDBC 5.5.0 接进 Spring Boot 项目。当时订单表已经跑到几千万行,单表索引层级加深,写入和查询都在明显变慢,DBA 给出的建议是尽快做水平拆分。翻了一圈资料,发现网上讲 5.x 配置的帖子要么停留在 4.x 的旧写法,要么直接甩官方文档让人自己啃,对刚从单表单库走过来的团队非常不友好。这篇就把我在 Spring Boot 项目里落地 ShardingSphere-JDBC 5.5.0 的基础配置过程完整写出来,包括依赖引入、YAML 规则、SQL 写法约束和实际排错记录,给准备上手分库分表的人一条能直接照着走的路。
1. 从单库到分表:我在什么场景下选了 ShardingSphere-JDBC 5.5.0
1.1 为什么要动分库分表这步险棋
先说说背景。我们订单中心原本是 MySQL 单实例单表,一天新增订单三十万左右,跑了两年多之后主表接近三千万行。索引从三层涨到四层,再加上订单查询走的是多条件组合,慢查询日志里开始频繁出现 1 秒以上的语句。这个时候常规的优化手段——加索引、调整 SQL、上缓存——都已经试过,收效不大。数据量摆在那里,单表的 B+ 树深度和随机 IO 成本已经很难靠单纯调优去弥补。
分库分表听着吓人,本质上是把一张大表按某个规则拆成多张小表,分散 IO 和写入压力。但它属于架构级改动,一旦上线,所有 SQL 都要重新审视有没有带上分片键,跨库查询和事务也都变得复杂。所以我的第一个建议是:如果你的数据量还在千万级以下,单库加好索引、读写分离能扛住,就别碰分库分表。 只有当单表数据量持续增长、写入吞吐遇到明显天花板时,才值得走这一步。
1.2 JDBC 模式与 Proxy 模式,为什么我选了前者
ShardingSphere 有两条路可以选:JDBC 模式和 Proxy 模式。JDBC 模式是引入一个增强版 JDBC 驱动 jar 包,直接嵌入到应用里,由应用自己完成 SQL 解析、路由和执行;Proxy 模式则是部署一个独立的代理服务,应用连接代理,代理再去连真实的数据库集群。
我在项目里选了 JDBC 模式,核心原因是团队运维能力有限,不想再多维护一套独立服务。两者的差异我整理了一张表:
| 对比项 | JDBC 模式 | Proxy 模式 |
|---|---|---|
| 部署形态 | jar 包嵌入应用,无需额外服务 | 独立代理服务,需要单独运维 |
| 性能损耗 | 较低,应用本地完成路由 | 多一层网络 IO,损耗稍高 |
| 语言支持 | Java 应用内嵌,天然友好 | 支持多种语言客户端 |
| 管理成本 | 无独立节点,升级随应用发版 | 需要监控、扩缩容代理节点 |
| 适用场景 | Spring Boot / Spring Cloud 等 Java 体系 | 多语言异构、DB 管控能力要求高的场景 |
对大多数中小团队来说,JDBC 模式足够用。这篇后面的所有配置也都基于 JDBC 模式。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 依赖与版本对齐:这一步没做对,后面全是坑
2.1 Maven 依赖到底怎么加
ShardingSphere-JDBC 5.5.0 的 Spring Boot Starter 坐标长这样:
xml复制<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-spring-boot-starter</artifactId>
<version>5.5.0</version>
</dependency>
注意 groupId 是 org.apache.shardingsphere,不是 io.shardingsphere。io.shardingsphere 是 4.x 的旧坐标,很多网上的老帖子还是在用这个,如果你复制了旧坐标再配上 5.5.0 的版本号,大概率会直接拉取失败,因为 io 坐标下根本没有 5.5.0 这个版本。
除了这个核心依赖外,你还要保证自己的数据源驱动和连接池在 pom 里,比如:
xml复制<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
连接池类型在后面的配置里会用到,一般直接用 HikariCP,Spring Boot 默认自带,不用额外引包。
2.2 Spring Boot 版本与 javax/jakarta 的兼容性
5.5.0 这个版本对 Spring Boot 2.x 和 3.x 都能兼容。但这里有个隐藏问题:Spring Boot 3.x 已经全面切换到 jakarta 命名空间,而 2.x 还是 javax,如果你在 2.x 项目里直接引入 5.5.0,而项目里有些老依赖还在用 javax.servlet 之类的 API,启动时可能会遇到类找不到或者冲突。
我们当时两个方案都验证过:
- Spring Boot 2.7.18 + JDK 8:最稳妥的旧项目组合,适合不想动 JDK 的老系统。
- Spring Boot 3.2.x + JDK 17:适合新项目,长期演进更顺。
关键点是确认整个依赖树里没有既带 javax 又带 jakarta 的重复类。最简单的方式是 mvn dependency:tree 看有没有多个 servlet API 冲突,如果有,用 exclusion 排除掉旧坐标即可。
3. 核心配置拆解:数据源、分片规则与主键生成
3.1 数据源配置:注意 jdbc-url 这个关键差异
ShardingSphere-JDBC 5.5.0 在 Spring Boot 里的配置统一挂在 spring.shardingsphere 前缀下。数据源配置和 Spring Boot 原生 spring.datasource 最大的区别是:这里每个数据源的连接地址属性名是 jdbc-url,不是 url。
下面是我们订单库拆分后的 YAML 配置:
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://192.168.1.10:3306/order_ds0?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: root123
max-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/order_ds1?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: root123
max-pool-size: 20
很多同事第一次配的时候直接把 spring.datasource.url 复制过来,启动直接报错:Failed to configure a DataSource: 'url' attribute is not specified。这里要记住,对 ShardingSphere 来说,它管理的这些多数据源是它自己来创建的,属性名用的是 jdbc-url,和 Spring Boot 原生配置不是一套体系。
3.2 分片算法与策略怎么组装
分片这块的配置是重头戏。ShardingSphere 5.x 把“分片算法”和“分片策略”拆成了两层:
- 分片算法:定义怎么算,比如 HASH_MOD 就是先哈希再取模。
- 分片策略:定义哪一列作为分片键,关联哪个算法。
我们的订单表设计是 2 个库(ds0、ds1),每个库 2 张表(t_order_0、t_order_1),按订单号取模分片。YAML 配置如下:
yaml复制spring:
shardingsphere:
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order_table_alg
key-generate-strategy:
column: order_id
key-generator-name: order_snowflake
sharding-algorithms:
order_table_alg:
type: HASH_MOD
props:
sharding-count: 4
key-generators:
order_snowflake:
type: SNOWFLAKE
props:
worker-id: 1
拆开解释一下上面的配置:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}是数据节点表达式,$->{0..1}表示从 0 到 1 枚举,最终会被展开成:ds0.t_order_0、ds0.t_order_1、ds1.t_order_0、ds1.t_order_1 这四个实际表。order_table_alg用的是 HASH_MOD 算法,sharding-count: 4表示结果取模 4,正好映射到 4 张表。这里算法结果要和 actual-data-nodes 里的表数量对得上,否则会出现路由到不存在的表。table-strategy.standard下面指定了sharding-column: order_id,意思是只有 SQL 里带有 order_id 等值条件时,才走分片路由。如果不带 order_id,ShardingSphere 没办法定位,就只能全库全表扫描,后文会展开说这个坑。
分片算法类型不止 HASH_MOD,常见还有 MOD、INTERVAL、CLASS_BASED 等。对于字符串分片键(比如用户 ID 前缀或者手机号),HASH_MOD 会比直接 MOD 更均匀,因为它是先哈希再取模,能避免像手机号前几位一样的数据聚集。
3.3 绑定表、广播表与分布式主键
如果你的业务里还有订单明细表 t_order_item,它和 t_order 一样按 order_id 分片,那一定要配置绑定表关系。绑定表的意思是:t_order 和 t_order_item 在 Join 时,保证相同 order_id 的数据落在同一个数据节点上,避免跨库关联。配置非常简单:
yaml复制spring:
shardingsphere:
rules:
sharding:
binding-tables:
- t_order,t_order_item
没配绑定表的后果很隐蔽:系统不会报错,但 Join 查询会产生笛卡尔积路由,比如 t_order 路由到 4 张表、t_order_item 也路由到 4 张表,最后拼出 16 个 SQL 去执行。数据量小的时候还能忍,一旦量大,性能直接崩。
广播表是另一种场景。比如订单系统里的支付渠道配置表 t_pay_channel,数据量只有几十行,但每个分片里的订单都要关联它。把这张表配成广播表,ShardingSphere 会在每个库都自动建一份相同的数据,查询时在本地库就能完成 Join:
yaml复制spring:
shardingsphere:
rules:
sharding:
broadcast-tables:
- t_pay_channel
广播表适用于字典表、配置表这类低频修改、全量数据小的表。注意高频更新的表不适合做广播表,因为每次写操作都要同步到所有库。
分布式主键这块,分库分表之后数据库自增主键彻底不能用了,因为每个库的自增序列独立,会出现重复 ID。上面配置里我用的 SNOWFLAKE,它会生成一个全局唯一的雪花 ID。配置项里的 worker-id 在多实例部署时要注意:尽量配成机器相关的唯一值,避免多实例同时生成时产生 ID 冲突。5.5.0 的 SNOWFLAKE 默认会根据 HostName 或者 IP 自动生成 worker-id,手工指定更可控。
4. SQL 写法与事务边界的适配
4.1 路由键设计:让 SQL 尽量携带分片键
配置完成后,最容易忽略的是 SQL 的写法约束。ShardingSphere 能帮你路由,但它不是万能的。核心原则是:能高效路由的 SQL,必须在 where 条件里带上分片键的等值条件。
拿我们的订单表举例,分片键是 order_id。下面这两种写法性能差异巨大:
sql复制-- 高效写法:带分片键等值条件,直接定位到单表
SELECT * FROM t_order WHERE order_id = 1001;
-- 低效写法:只有时间条件,全库全表扫描
SELECT * FROM t_order WHERE create_time > '2024-01-01';
第二种写法不会报错,但 ShardingSphere 没办法判断数据落在哪张表,只能把 SQL 广播到 4 张表都执行一遍再合并结果。如果只是偶尔一次还能接受,要是高频查询都这样,分库分表带来的性能收益就被吃光了。
实际业务里还有一个更头疼的问题:用户查询订单通常走的是 user_id,而分片键是 order_id。这两个都对不上,怎么办?我们采用的是订单关联表方案:单独建一张 t_order_user_mapping,以 user_id 为分片键,冗余存储 order_id 和 user_id 的映射关系。查询时第一步先通过 user_id 查映射表拿到 order_id 列表,第二步再按 order_id 去查真实订单表。这样两次查询都带了分片键,全程没有全路由。
还有一点要注意:插入操作必须带分片键。 如果 insert 语句里没有 order_id 的值,ShardingSphere 直接抛异常,根本不会执行。这个异常信息比较直白,基本一眼就能定位。
4.2 事务边界处理:从本地事务到跨库事务
分库分表之后,事务是最容易出问题的环节。默认情况下 ShardingSphere-JDBC 沿用 Spring 的本地事务,只对当前数据源生效。如果你的一个业务操作同时更新了 ds0 和 ds1 里的订单,普通 @Transactional 是管不住跨库一致性的。
ShardingSphere 5.5.0 自带基于 XA 的分布式事务能力。用法是在方法上同时加两个注解:
java复制@Transactional
@ShardingSphereTransactionType(TransactionType.XA)
public void createOrderAndAudit(OrderDO order) {
orderMapper.insert(order);
auditMapper.insert(orderAudit);
}
这里的 TransactionType.XA 走的是 Atomikos 之类的 XA 事务管理器。XA 能保证跨库最终一致,但代价也不小,事务提交时间明显变长,而且对数据库本身也有要求。所以在实际项目中,我们的准则是:尽量把事务控制在单分片内。 设计表结构时让强一致性的数据都落在同一个分片上(比如同一个用户的订单和支付记录都用 user_id 分片),需要跨库的业务改成异步消息、对账补偿等方式,而不是把宝都押在分布式事务上。
5. 实测验证与排错:让配置真正跑起来的调试记录
5.1 三步验证分片是否生效
配置写完后,第一件事不是写业务接口,而是验证分片逻辑到底有没有按照预期走。我的习惯是三步走:
第一步:开启 SQL 日志。
yaml复制spring:
shardingsphere:
props:
sql-show: true
sql-show: true 会在日志里输出逻辑 SQL 和真实 SQL。日志里会看到类似这样的内容:
text复制ShardingSphere-SQL: Logic SQL: SELECT * FROM t_order WHERE order_id = ?
ShardingSphere-SQL: Actual SQL: ds0 ::: SELECT * FROM t_order_0 WHERE order_id = ?
如果 Actual SQL 只路由到 ds0 下的 t_order_0,说明分片正确。如果看到四条实际 SQL 分别打到四个表,那就要检查是不是没带分片键。
第二步:确认数据实际落到了哪个物理表。
在分片配置正确的情况下,插入一条测试数据,然后直接去 MySQL 里看 t_order_0 和 t_order_1 各自有哪些记录。按 HASH_MOD 取模,order_id 对 4 取模是几,就应该落在编号为几的表里。手动核对一次,比看一百遍日志都踏实。
第三步:查执行计划。
sql-show 只能看到路由结果,看不到性能。对于慢查询,我会在 MySQL 端开启慢查询日志,配合 EXPLAIN 看实际表索引使用情况。分片之后单表数据量小了一半,之前常见的 filesort 和全表扫描如果还在,说明 SQL 写法有问题,需要回看第 4 章的约束。
5.2 高频报错清单与根因分析
几个星期跑下来,我们遇到的高频报错和排查结果整理成了下面的表,给后来人省点时间:
| 报错现象 | 根因 | 解决办法 |
|---|---|---|
启动报 Failed to configure a DataSource |
数据源属性写成了 url | 改成 jdbc-url |
启动报 Can not find table |
逻辑表没有匹配到任何 actual-data-nodes | 检查 actual-data-nodes 表达式,确认表名和数据节点范围 |
运行报 Inline sharding algorithm expression cannot be parsed |
内联表达式写错,比如少了 $->{} |
按 ds_$->{0..1} 格式检查表达式 |
insert 时报 Sharding value must be provided |
插入语句没带分片键 | 调整 SQL,带上 order_id 等分片键 |
| 跨库查询结果不对 | 绑定表没配置,关联查询产生笛卡尔积 | 配置 binding-tables |
| 分布式主键报 worker-id 冲突 | 多实例配置了相同的 worker-id | 改成基于 IP 或 HostName 生成,或手工指定全局唯一值 |
其中一个比较典型的排查过程值得单独说说。当时我们新加了一张 t_order_refund 退款表,actual-data-nodes 写成了 ds$->{0..1}.t_order_refund_$->{0..1},但实际上这张表只在 ds0 和 ds1 各建了一张,没有建 t_order_refund_0 和 t_order_refund_1。启动不报错,一插入就报 Table 'ds0.t_order_refund_0' doesn't exist。排查时我一度以为是建表脚本漏了,后来才发现是数据节点表达式跟实际物理表对不上。所有分表在创建时就要和 actual-data-nodes 表达式严格对齐,少一张表或者多一张表都会在运行期暴露问题。
6. 上线之后的一些反思与建议
6.1 分片键的选择建议
分片键是整个分库分表设计里最重要、也最难改的决定。一旦业务跑起来,再换分片键基本等于重构。几点经验:
- 选业务里查询频率最高的等值条件。 如果订单系统 80% 的查询都带 user_id,那就优先考虑 user_id 分片;如果按 order_id 才能保证数据分布均匀,就配映射表兜底。
- 选分布均匀的列。 如果分片键本身有严重热点,比如只有少数几个值占了大头,取模后热点数据还是会集中在同一个分片,达不到分散效果。
- 避免用会变的列。 分片键的值一旦修改,数据迁移成本极高,所以像手机号这种可能更换的字段要慎重。
6.2 配置管理与演进思路
基础配置跑通只是第一步。实际项目里我会把 ShardingSphere 的 YAML 配置单独抽出来,放到 Nacos 配置中心管理,而不是散落在各个应用的本地配置文件里。这样分片规则变了不需要改代码重新发版,只要在配置中心调整后刷新即可。
还要提前想好扩容问题。我们预留了 ds0、ds1 两个库,后续如果数据量翻倍,可以再加一组 ds2、ds3。但 ShardingSphere 的分片算法不会自动感知新节点,扩容需要配合数据迁移工具把存量数据按新分片规则重新打散。这块超出基础配置范畴,但在设计之初就要把容量规划想清楚,否则后期只能在业务低谷期停机迁移。
最后说一点个人感受:分库分表这件事,技术上并不是高不可攀,ShardingSphere-JDBC 5.5.0 和 Spring Boot 的集成已经做得足够简单,真正的难点在业务改造和运维习惯。配置规则只是第一步,更多的工作是让团队明白"每条 SQL 都要考虑分片键""跨库操作要尽量避免",这些代码规范层面的事情,比堆配置更难,也更需要从第一天就抓起。
