如果你的项目里,单表数据量已经跑到千万级甚至亿级,写入并发一上来,数据库的CPU和IO就开始报警了,那么分库分表这件事,迟早要面对。我目前在生产的核心订单链路用的就是 ShardingSphere-jdbc 5.5.0 + Spring Boot 这套组合,从单库单表平滑切到分库分表,整体改动量和风险控制都还算理想。这篇就专门写给那些正准备上手 ShardingSphere-jdbc 的 Java 后端同学,把从依赖引入、配置编写到核心代码改造的完整链路走一遍,并且把我实际踩过的坑也一并交代清楚。
如果你是第一次接触 ShardingSphere-jdbc,也没关系,这篇文章不假设你有任何分库分表基础,只要求你熟悉 Spring Boot 和 MyBatis 的基本使用。我会把逻辑表、真实表、分片算法、分布式主键这些容易绕晕的概念,用最直白的方式讲明白。看完之后,你应该能独立完成一套可运行的基础分库分表配置,并能根据业务需求扩展出读写分离、强制路由、绑定表等高阶玩法。
1. 分库分表整体思路与版本选型
1.1 为什么是 ShardingSphere-jdbc 而不是 ShardingSphere-proxy
先解决一个最常见的疑问:同样是 ShardingSphere 生态,有 jdbc 和 proxy 两种形态,到底选哪个。它们俩的定位差异非常本质,搞清楚了,你后续的架构方向就不会跑偏。
ShardingSphere-jdbc 是客户端模式,本质是一个增强版的 JDBC 驱动。你的应用直接连它,它内部帮你管理多个真实的数据库连接,然后把你在代码里写的 SQL 做解析、改写、路由、归并,最后把结果返回给你。从应用视角看,它就是一个普通数据源。好处是性能损耗极低,因为不需要额外的网络跳转,而且可以拿到应用线程上下文,做 hint 强制路由之类的精细化控制。
ShardingSphere-proxy 是服务端模式,它独立部署成一个代理服务,你的应用连的是 proxy,proxy 再连真实的数据库。好处是异构语言友好,不用改应用代码,但多一跳网络,延迟会高一些,而且在复杂查询的归并能力上,jdbc 模式依然更强一些。
我的建议很直接:如果团队技术栈统一是 Java,且对性能敏感,无脑选 jdbc 模式。这篇文章所有配置也是基于 ShardingSphere-jdbc 来写的。另外补充一句,ShardingSphere-jdbc 5.x 的版本号演进很快,5.5.0 是目前比较稳定的一个版本,修复了不少之前的 NPE 和配置兼容问题,锁这个版本没毛病。
1.2 核心概念扫盲:逻辑表、真实表、数据节点
新手第一次看 ShardingSphere 的配置,十有八九被一堆“表名”搞晕。这里我用订单表来举例,一次性讲透。
- 真实表(actual table):数据库里真实存在的表,比如
t_order_0、t_order_1、t_order_2、t_order_3,这就是你在 MySQL 里实际建的表。 - 逻辑表(logic table):你在代码里操作的表名,比如
t_order。你的 SQL 只写select * from t_order where order_id = 1,至于它实际落到哪张真实表,由 ShardingSphere 根据分片算法算出来。 - 数据节点(data node):真实表的位置描述,通常写成
db$->{0..1}.t_order_$->{0..3},这个表达式表示两个库(db0、db1),每个库 4 张分片表,一共 8 个数据节点。
理解这三者的关系,是配置分片规则的第一步。逻辑表是你的业务视角,真实表是存储视角,数据节点是映射关系。配置的核心就是告诉 ShardingSphere:逻辑表t_order对应哪些真实表,以及用哪一列、按什么算法算出路由目标。
1.3 选用标准分片策略还是自定义策略
ShardingSphere 5.x 内置了多种分片算法,其中最常用的是HASH_MOD(哈希取模)和INLINE(Groovy 表达式),另外还有MOD、RANGE_MOD、COMPLEX_INLINE、HINT_INLINE等。实际项目里,大部分场景用HASH_MOD就够了,它会把分片键的哈希值对分片总数取模,分布相对均匀。
不过我建议你在正式上生产前,一定要考虑“分片键选择”和“数据增长”这两个问题。比如你用order_id做分片键,那所有查询最好都带上order_id,否则就走全路由,性能大打折扣。数据量增长后,初始分片数是 8 个,想扩到 16 个,HASH_MOD会面临大规模数据迁移。这个时候可以在设计初期就预估三年的数据量,把分片数一次性定得大一点,比如 64 或 128,避免中途扩容。基础配置阶段用HASH_MOD完全没问题,但脑子里要绷着这根弦。
2. 环境准备与依赖引入
2.1 软件版本对照与兼容性说明
先说环境版本,这地方踩坑的人特别多。ShardingSphere-jdbc 5.5.0 对 Spring Boot 的版本兼容性是没有问题的,Spring Boot 2.7.x 和 3.x 都能用,不过两者引入的依赖坐标不同,下面会细说。数据库建议 MySQL 5.7 及以上,驱动用mysql-connector-j8.0.33 或更高版本。JDK 至少 8,如果是 Spring Boot 3.x,那 JDK 要求 17。
这里必须提醒一个老坑:网上大量教程还在教com.mysql.jdbc.Driver,这个类在 MySQL Connector/J 8.x 里已经废弃了,正确写法是com.mysql.cj.jdbc.Driver。配置错了,项目启动就会报ClassNotFoundException,而且报错信息不够直观,很容易让人怀疑是 ShardingSphere 的锅,实际上是驱动类名的问题。
我的测试环境如下,建议你直接用这个组合,省心:
| 组件 | 版本 |
|---|---|
| JDK | 1.8 或 17 |
| Spring Boot | 2.7.18 或 3.2.x |
| ShardingSphere-jdbc | 5.5.0 |
| mysql-connector-j | 8.0.33 |
| MyBatis-Plus | 3.5.5(可选) |
| HikariCP | Spring Boot 内置 |
2.2 Maven 依赖坐标的坑与正确姿势
ShardingSphere-jdbc 5.5.0 的 Maven 坐标有变化,如果你的项目是 Spring Boot 3.x,那依赖要用shardingsphere-jdbc-spring-boot-starter,4.x 时代的shardingsphere-jdbc-core-spring-boot-starter在 5.x 已经不存在了。
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-spring-boot-starter</artifactId> <version>5.5.0</version> </dependency>如果你用的是 Spring Boot 2.x,官方文档建议使用shardingsphere-jdbc-spring-boot-starter搭配spring-boot-starter-jdbc。我的实际经验是直接引入上面这个坐标,然后确认项目中已经有spring-boot-starter-jdbc或mybatis-spring-boot-starter即可,一般不会冲突。
另外要说一个细节:ShardingSphere-jdbc 5.5.0 会和 Druid 连接池打架。如果你以前的项目用了 Druid,引入 ShardingSphere 后启动时会报数据源类型错误。建议要么改用 HikariCP(Spring Boot 默认),要么在配置里显式指定type: com.zaxxer.hikari.HikariDataSource。这块我踩过一次,后续都在配置里统一写了 Hikari,再没出过问题。
2.3 前置准备:两个库 8 张表的建表脚本
为了演示效果,我准备了一个经典的订单场景:两个数据库db0和db1,每个库各 4 张订单分片表,另加一个广播表t_dict用于字典数据同步。
先创建数据库:
CREATE DATABASE IF NOT EXISTS db0 DEFAULT CHARSET utf8mb4; CREATE DATABASE IF NOT EXISTS db1 DEFAULT CHARSET utf8mb4;每个库中创建订单表,注意真实表名是t_order_0到t_order_3:
CREATE TABLE IF NOT EXISTS db0.t_order_0 ( order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, order_amount DECIMAL(10,2) DEFAULT NULL, status INT DEFAULT NULL, create_time DATETIME DEFAULT NULL, PRIMARY KEY (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;三张表的建表语句除了表名不同,结构完全一致。db1库中也执行同样的四张表。另外每张表都要记得加上普通索引,尤其是按user_id查询的索引,否则分片后查询效率会很难看。分库分表只能通过分片键做到局部路由,真正落到单表后的查询优化还是得依靠索引。
广播表t_dict也需要在两个库中都创建:
CREATE TABLE IF NOT EXISTS db0.t_dict ( dict_id BIGINT NOT NULL, dict_type VARCHAR(32) DEFAULT NULL, dict_value VARCHAR(128) DEFAULT NULL, PRIMARY KEY (dict_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3. 基础配置实战:从单库到分库分表
3.1 数据源配置:多数据源的定义方式
先看 Spring Boot 的application.yml完整配置。注意,一旦使用 ShardingSphere-jdbc,你的数据源就不再是直接配置在 Spring 容器里的单个数据源了,而是全部收编到spring.shardingsphere.datasource下面。原有的spring.datasource.url配置要删掉或注释掉,否则会出现数据源重复初始化的问题。
spring: shardingsphere: datasource: names: db0, db1 db0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/db0?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: root123 max-pool-size: 20 min-pool-size: 5 db1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: root123 max-pool-size: 20 min-pool-size: 5 rules: sharding: # ... 分片规则,下面详解 props: sql-show: true这里的jdbc-url不是url,我自己第一次写的时候写成url,启动直接报错。另外,max-pool-size和min-pool-size是 HikariCP 的参数,建议每个库的连接数不要设得太大,因为分库后连接数是乘以库数量的,2 个库就是双倍连接。如果你的服务实例很多,连接数控制不好,数据库会被打满连接。生产上单库 20 个连接足够,没必要追求大池子。
3.2 分片规则配置:分片算法、分片策略与绑定表
现在到最关键的部分。分片规则配置里同时包含了分片算法、分片策略、绑定表、广播表等信息。我把配置写全,然后逐一拆解。
spring: shardingsphere: datasource: names: db0, db1 # ... 上面已给出 rules: sharding: tables: t_order: actual-data-nodes: db$->{0..1}.t_order_$->{0..3} table-strategy: standard: sharding-column: order_id sharding-algorithm-name: t_order_hash_mod key-generate-strategy: column: order_id key-generator-name: snowflake t_order_item: actual-data-nodes: db$->{0..1}.t_order_item_$->{0..3} table-strategy: standard: sharding-column: order_id sharding-algorithm-name: t_order_hash_mod key-generate-strategy: column: order_id key-generator-name: snowflake binding-tables: - t_order, t_order_item broadcast-tables: - t_dict sharding-algorithms: t_order_hash_mod: type: HASH_MOD props: sharding-count: 8 t_user_id_mod: type: HASH_MOD props: sharding-count: 8 key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1 props: sql-show: true逐个解释。actual-data-nodes定义了逻辑表映射到哪些真实表,db$->{0..1}是db0、db1的简写,t_order_$->{0..3}是四张真实表。这个表达式看起来像正则,实际是 Groovy 模板语法,注意中间不要随便加空格,否则解析直接失败。
table-strategy里定义的standard策略,表示标准分片,用单分片键。sharding-column: order_id指定分片键,sharding-algorithm-name指向下方自定义的算法t_order_hash_mod。算法类型用HASH_MOD,配置sharding-count: 8,也就是把所有数据均匀分布到 8 张分片表中。
注意,这里的分片算法是按全量 8 张表来取模的,那怎么确定数据落到哪个库呢?其实 ShardingSphere 的默认数据节点路由不区分库,8 个数据节点就是 8 张表。如果你想按user_id再做一次库路由,比如db$->{user_id % 2},那就需要自定义多分片键或复合分片算法。基础配置阶段,用 8 个数据节点的HASH_MOD已经足够跑通全流程,但真实业务往往会有更复杂的路由需求,比如先按用户维度分库,再按订单维度分表。
binding-tables非常关键,一定要配置。绑定表是指分片规则完全一致的一组表,例如t_order和t_order_item都按order_id分片,那么它们 join 查询时,ShardingSphere 可以保证关联数据落在同一个数据节点上,避免笛卡尔积跨库 join,性能差别巨大。不配置绑定表,join 查询会把 8 张订单表与 8 张订单明细表做全组合关联,SQL 会被改写得极其恐怖。
broadcast-tables是广播表,通常放字典表、配置表这类小表。广播表会在每个库中都保留一份完整数据,写入时同步到所有库,查询时随机路由到任意一个库。
3.3 读写分离场景下的数据源配置追加
如果你的库已经做了主从复制,想在分库分表的基础上叠加读写分离,配置也不复杂。核心点在于,先定义一个逻辑数据源,比如ds_0,这个数据源包含写库和读库,然后分片规则里的数据节点引用这个逻辑数据源。
spring: shardingsphere: datasource: names: db0, db0_read, db1, db1_read db0: type: com.zaxxer.hikari.HikariDataSource # ... db0_read: type: com.zaxxer.hikari.HikariDataSource # ... # db1、db1_read 同理 rules: readwrite-splitting: >@TableName("t_order") public class Order { @TableId(type = IdType.INPUT) private Long orderId; private Long userId; private BigDecimal orderAmount; private Integer status; private LocalDateTime createTime; }注意两个细节。第一,@TableName里必须写逻辑表名t_order,不能写真实表名t_order_0。第二,主键类型要用IdType.INPUT,因为主键由 ShardingSphere 的分布式主键生成器来生成,如果还让它走 MyBatis-Plus 的默认自增策略,会与 ShardingSphere 的主键生成逻辑冲突。我在 MyBatis-Plus 3.5.x 下实测,如果设置成ASSIGN_ID,虽然 MyBatis-Plus 会生成雪花 ID,但 ShardingSphere 也会尝试生成,结果就是主键列被覆盖或插入报错。
Mapper 接口写法不用变:
@Mapper public interface OrderMapper extends BaseMapper<Order> { List<Order> selectByUserId(@Param("userId") Long userId); }XML 里的 SQL 同样只写逻辑表名:
<select id="selectByUserId" resultType="com.example.demo.entity.Order"> select * from t_order where user_id = #{userId} </select>这里必须提醒:如果你在 XML 里写了t_order_0这样的真实表名,ShardingSphere 照样会解析和改写,但路由规则会变成“指定表路由”,也就是说它不再根据分片键计算,而是直接路由到你写死的那张表。这在某些特殊场景是故意为之,但在业务开发中大概率是个 bug,因为一旦真实表名写错,数据就查不到或者插错了。
4.2 分布式主键的配置与踩坑记录
分库分表之后,数据库自增主键彻底失效,因为每个库的自增 ID 会重复。ShardingSphere 默认提供雪花算法(Snowflake)作为分布式主键生成器,配置方式上面已经给出了。
雪花算法生成的 ID 是一个 64 位 Long 型整数,由时间戳、机器 ID、序列号组成。它在基础配置阶段使用起来很简单,但有两个隐患必须提前知道。
第一个隐患是时钟回拨。如果部署应用的机器出现 NTP 时间回拨,雪花算法可能生成重复 ID。ShardingSphere 5.5.0 对时钟回拨有一定处理,但并不能保证 100% 安全。生产环境建议对应用服务器做时间同步配置,避免时间跳跃。
第二个隐患是前端精度丢失。雪花 ID 是 19 位 Long,JavaScript 的 Number 类型只能安全表示 2^53 以内的整数,19 位 ID 传给前端会丢失精度。解决方案有两种:一种是在后端序列化时转成 String,这个在 Jackson 里配置一下就行;另一种是干脆不用雪花算法,改用UUID或自定义的号段模式。我用的是雪花 ID 转字符串的方案,侵入性最小。
4.3 写入数据的完整调用链路
为了让新手对整体流程有个直观感受,我给一个完整的 Service 层写入例子:
@Service public class OrderService { @Resource private OrderMapper orderMapper; public void createOrder(Order order) { order.setOrderId(null); order.setCreateTime(LocalDateTime.now()); orderMapper.insert(order); } }如果orderId为 null,ShardingSphere 会调用配置好的snowflake生成器生成主键。如果orderId不为 null,则直接用传入值作为主键。这里有一个容易被忽略的细节:分片键order_id如果是传入的,那么 ShardingSphere 不会对该值做合法性校验,它只负责拿这个值做哈希取模路由。所以你的代码里必须保证orderId有值且全局唯一,否则插入后会出现主键冲突,或者路由到错误的表。
写入完成后,可以用sql-show日志确认实际插入到了哪张表。我在本地测试时经常看到类似这样的输出:
Actual SQL: db1 ::: insert into t_order_3 (order_id, user_id, order_amount, status, create_time) values (7834019283741016064, 1001, 199.00, 0, 2025-01-01 12:00:00)如果日志显示的表名和数据量级符合预期,说明整条链路已经跑通了。接下来就可以开始做各种复杂查询的验证了。
4.4 绑定表 join 查询的代码示例
基础功能跑通后,很多人第一个面临的需求就是订单表和订单明细表的 join 查询。如果没有配置绑定表,这个查询会被改写成 8 张订单表 × 8 张订单明细表的全排列,SQL 膨胀到几十行,执行效率惨不忍睹。
配置了绑定表之后,代码完全不用特殊处理:
@Mapper public interface OrderItemMapper extends BaseMapper<OrderItem> { // 继承 BaseMapper 即可 }写 XML 时照常 join:
<select id="selectOrderWithItem" resultType="map"> select o.order_id, o.user_id, i.item_name from t_order o inner join t_order_item i on o.order_id = i.order_id where o.order_id = #{orderId} </select>关键点在于:join 的关联字段必须是分片键。这样 ShardingSphere 才能根据order_id把两个逻辑表映射到同一个真实数据节点上,join 操作在单库内完成,性能最优。如果你的 join 字段不是分片键,绑定表配置就形同虚设,跨库 join 的问题会重新暴露出来。这个设计约束最好在表结构设计阶段就想清楚。
5. 常见问题与排查技巧实录
5.1 启动报错:Data source is not supported
这是一个出现频率极高的报错。启动时类似:
Caused by: org.apache.shardingsphere.infra.exception.core.external.sql.type.generic.UnsupportedSQLOperationException: Data source is not supported我见到这个报错通常会按两步排查。第一步,看配置里type字段是否写的是com.zaxxer.hikari.HikariDataSource。如果是druid且项目里没有引入 Druid 依赖,或者类型名写错,就会报不支持。第二步,确认是否同时保留了原生的spring.datasource.url配置。ShardingSphere-jdbc 启动时会尝试接管数据源,如果检测到多个数据源定义,就会发生冲突。把原生数据源配置注释掉,只保留spring.shardingsphere.datasource下的定义,重启一般就正常了。
5.2 数据查不到或写错库的排查思路
这类问题最隐蔽,症状是应用不报错,但数据没有出现在你预期的表里。排查思路按以下顺序来:
- 确认
sql-show日志里的 Actual SQL 走的是哪张表。 - 核对
actual-data-nodes里的库名、表名是否与实际数据库一致,尤其注意db$->{0..1}这个写法,括号里是从 0 开始的下标,不是库名前缀。 - 确认分片键的值类型。
order_id如果是 String 类型,而配置的分片算法是HASH_MOD,那么取模的哈希值是基于字符串的,可能与数值类型的预期不一致,导致分布不符合直觉。解决方案是让分片键类型统一,或者在算法层做类型转换。 - 检查是否存在广播表污染。如果
t_dict这类表没有出现在广播表配置里,它会被当成普通逻辑表来处理,写入时只会路由到一张真实表,其他库查不到数据。
5.3 SQL 报错:Table not found 与逻辑表名
有时候 SQL 里明明写的表名没有问题,但 ShardingSphere 却报 Table not found。这种情况多半是因为你在 SQL 里使用了数据库名前缀,比如:
select * from db0.t_order_0 where order_id = 1一旦 SQL 里带了db0和真实表名t_order_0,ShardingSphere 会认为你已经指定了物理库表,不会再做逻辑路由。但如果你拿这个 SQL 去查db1库里的数据,自然就查不到,甚至直接报表不存在。解决办法是,业务 SQL 永远只写逻辑表名,不加库名前缀,把路由的事完全交给 ShardingSphere。
5.4 分片键缺失导致的全路由查询性能瓶颈
最后聊一个性能问题。当你的查询条件里没有分片键时,比如:
select * from t_order where user_id = 1001ShardingSphere 无法根据分片键定位到具体数据节点,只能把这条 SQL 广播到所有 8 张真实表上执行,再把结果合并返回。这在数据量小的时候感觉不出来,表一多、数据一多,性能会急剧下降。优化手段一般是:让按照用户维度查询的场景直接以user_id作为分片键重新设计分片策略,或者引入user_id -> order_id的映射表,先查到order_id列表再走分片键查询。
基础配置阶段不用过于追求极致的查询性能,但在建表设计时一定要意识到分片键选的越贴近核心查询维度,后续的 SQL 就越容易写、越容易优化。
5.5 旧资料与 5.x 版本的配置差异提醒
现在的网上的教程鱼龙混杂,很多还是 4.x 的写法。4.x 时代的分片配置前缀是spring.shardingsphere.rules.sharding,5.x 已经移除了rules之前的错误前缀,正确的路径是直接在spring.shardingsphere.rules.sharding下配置,别被旧文章带偏了。此外,5.x 的分片算法类型名也变了,比如inline改成了INLINE,hash_mod改成了HASH_MOD,大小写不敏感但最好按官方文档来。还有一个差异是 5.x 版本用sharding-algorithms来配置算法,4.x 里是写在props里的,结构完全不同。遇到配置不生效时,先去官方文档确认一下当前版本的 YAML 结构,网上随便找的配置大概率已经过时。
写在最后
说实话,ShardingSphere-jdbc 这套东西,边界情况非常多,光靠配置一次就跑通是不现实的。我自己在生产上踩过最狠的一个坑,是绑定表配置漏了导致 join 查询爆炸,排查了整整一个下午。所以如果你刚开始接触,我的建议是:先不要直接在你的核心业务表上动刀,找一张低频的表,按照这篇文章从头到尾跑一遍,把路由原理和配置结构摸清楚,再逐步扩大到核心表。另一个建议是,分库分表属于一旦上线就很难回退的架构改造,上线之前一定要想清楚容量规划、数据迁移方案和灰度方案,千万不要抱着“先上线再优化”的心态。希望这篇实战记录能帮你少走一些弯路。