☰
Spring Boot 整合 ShardingSphere-JDBC 与 MyBatis 分库分表实战
2026/10/12 2:49:39 网站建设 项目流程

简介:这是一份基于SpringBoot、ShardingJDBC与MyBatis的分库分表整合示例资源,面向正在学习分布式数据库设计或需要落地分库分表方案的Java后端开发者。资源核心价值在于展示三个框架如何协同工作:ShardingJDBC负责水平分片与路由,MyBatis完成ORM映射,SpringBoot提供自动配置,从而帮助理解用户ID哈希取模、数据源动态选择及事务处理等关键实践。压缩包共15个文件,其中5个Java源码、2个XML映射配置、1个YAML配置文件,另含SQL初始化脚本、Maven打包脚本及说明文档,整体仅61KB,轻量精炼。目前已有187人学习下载,适合作为入门分库分表的参考项目,通过阅读源码与配置即可快速掌握分片规则设定和集成要点。

1. 分库分表落地:为什么我用 ShardingSphere-JDBC 而不是中间件

做了几个月的订单模块,表数据一过千万,单库单表的查询就直接被打回原形,慢查询日志里全是扫描全表的 SQL。之前考虑过独立的中间件方案,但一想到要多维护一套高可用集群,还得改应用层的连接方式,就把目光放回了 ShardingSphere-JDBC。它本质上是一个轻量级的 Java 框架,以 jar 包形式嵌在应用里,直接走 JDBC 协议,项目不用额外部署服务。Spring Boot 项目里引入依赖、写好分片规则,MyBatis 的 Mapper 接口不改一行,就能把订单数据打散到多库多表中去。这份资源就是一套完整的 springboot + shardingjdbc + mybatis 工程骨架,适合手里有存量单体应用、想低成本切入分库分表的 Java 开发者。

2. 工程结构与核心依赖:先看清分片引擎怎么加载

2.1 源码目录与启动入口

这类分库分表项目拿到手之后,不要急着改配置。先花几分钟把工程结构捋一遍,搞清楚哪些是框架自动装配的,哪些是业务自己写的。这套工程用 Maven 管理,标准的 Spring Boot 父子结构,启动类放在主模块下。我拆过几个类似的包,目录大同小异,核心就几块:

springboot_shardingjdbc_mybatis ├── pom.xml ├── sql/ │ ├── init_database.sql # 建库建表脚本 │ └── init_data.sql # 初始化演示数据 ├── src/main/java │ ├── Application.java # Spring Boot 启动类 │ ├── controller/ # 测试用 HTTP 入口 │ ├── mapper/ # MyBatis Mapper 接口 │ ├── service/ # 业务层:写库、读库、事务测试 │ └── config/ # 分片/读写分离配置类(如果有) └── src/main/resources ├── application.yml # 数据源与分片规则核心配置 ├── mapper/ # MyBatis XML 文件 └── sharding-jdbc.yml # 部分版本会把分片规则拆出来(可选)

重点看application.yml,这里几乎是 ShardingSphere-JDBC 的“黑匣子”入口。框架启动时会读取spring.shardingsphere前缀下的配置,把数据源列表、分片算法、主键生成策略全部装载进上下文。要注意一点:如果你的工程里同时有多个DataSource自定义配置,ShardingSphere 可能会出现数据源冲突,导致启动报circular reference,这个问题后面避坑章详细说。

2.2 Maven 依赖选型

ShardingSphere-JDBC 的版本演进很快,从 3.x 到 5.x 的 API 差异不小。这套工程基于 5.x 版本,正好是对齐当前主流 Spring Boot 2.7 生态的。核心依赖就两个:

<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.1.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.8</version> </dependency>

值得单独解释的是为什么还需要一个连接池依赖。ShardingSphere-JDBC 本身不负责实际连接的管理,它只做 SQL 解析、路由和执行结果归并,最终还是要落在真实的数据源上。所以工程里会配一个 Druid 或 HikariCP 作为底层连接池。在 5.x 配置里,每个物理数据源的数据源类型就是com.alibaba.druid.pool.DruidDataSource。我见过的第一次启动失败案例,八成是漏了这个连接池依赖,或者连接池类型配置和实际依赖对不上。

3. 分片规则配置实战:哈希取模和时间范围怎么选

3.1 分片键与分片算法

分片规则是整个分库分表项目的灵魂。配置错了,可能启动起来测试发现数据全落在同一张表里,那分库分表就没有任何意义。这套工程的演示场景是订单表,表结构写得很简单:id(分布式主键)、order_no、user_id、amount、create_time。分片键选的是user_id,这是一个非常标准的选择——订单查询的高频场景就是按用户维度来查,把同一用户的订单落在同一张表里,能最大化避免跨表扫描。

算法上是哈希取模,分别对库和表各做一次取模。库数量 2,表数量 2,分成ds0、ds1两个库,每个库里都有t_order_0、t_order_1两张表。取模公式就是user_id % 2,这里有个隐含的注意点:分库和分表都用同一个分片键,但路由结果要保证均匀。如果库容量和表容量成倍数关系,容易出现“数据倾斜”,例如user_id的尾数全是偶数,那ds0就扛了大量数据。所以工程里不是简单% 2,而是库取模是user_id % 4再除以 2 之类的策略(后面代码里能看到),目的是让分布更散。

application.yml里的关键配置片段如下:

spring: shardingsphere: datasource: names: ds0, ds1 ds0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ds0?serverTimezone=Asia/Shanghai&useSSL=false username: root password: root ds1: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ds1?serverTimezone=Asia/Shanghai&useSSL=false 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_hash table-strategy: standard: sharding-column: user_id sharding-algorithm-name: table_hash sharding-algorithms: db_hash: type: HASH_MOD props: sharding-count: 2 table_hash: type: HASH_MOD props: sharding-count: 2

强调一下actual-data-nodes这个配置。ds$->{0..1}.t_order_$->{0..1}是 ShardingSphere 的内联表达式,表示匹配ds0、ds1两个库下各两张物理表。如果你物理表没有建全,配置的是 2 表但实际只有 1 表,SQL 路由过去会直接报Table does not exist。我第一次拆分包时把表建错名字,排查了很久才发现是数据节点和物理表对不上。

3.2 分布式主键生成策略

单库单表时代可以依赖数据库自增主键,分库分表之后这个方案就不成立了。两个库的主键都从 1 开始自增,必然撞车。ShardingSphere 提供了内置的分布式主键算法,最常用的是SNOWFLAKE雪花算法。工程里的配置也反映了这一点:

key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1 # 注意:雪花算法生成的 ID 是 19 位 long,后端用 Long 接收,前端 JS 精度会丢

worker-id在单机部署时填一个固定值就够了,如果后面扩到多节点,每个应用实例的worker-id必须不同,否则会生成重复 ID。这个参数在分布式环境里是“必改项”,很多第一次上手的人会忽略。

雪花算法生成的 ID 是长整型,落到 JSON 里传给前端,JavaScript 的Number类型会丢失精度,导致最后几位变成 0。解决方法是让工程统一把分布式 ID 转成 String 序列化,或者在数据库层面直接用varchar存储主键。我习惯直接建表时把id定义成bigint,在 JSON 序列化配置里加一个 Long 转 String 的规则,这样既保证数据库排序性能,又不影响前端取值。

3.3 绑定表与广播表

分片之后最怕跨库 JOIN。t_order和t_order_item如果都按user_id分片,理论上可以关联查询,但 ShardingSphere 必须知道这两张表的切分方式一致,才能把 SQL 路由到同一个物理库的物理表对上去做 JOIN。配置里就要声明绑定表关系:

binding-tables: - t_order, t_order_item

如果不声明绑定表,执行跨表 JOIN 时 ShardingSphere 会把 SQL 发给所有数据节点,然后在内存里做结果归并。多表 JOIN 的性能下降非常明显,数据量一大就容易撑爆内存。这个配置看着不起眼,但对查询性能影响很大。

广播表是另一种情况:字典表、配置表这类数据量小、基本不变化、所有分片都需要完整副本的表。工程里把t_dict配成了广播表,规则就是路由时复制到所有分片,每次写操作都会同步写全部库表:

broadcast-tables: - t_dict

这里有一个明显的坑:广播表的写操作耗时是普通表的 N 倍(N 等于分片数量),而且不支持分布式事务。所以广播表只适合低频写、高频读的数据,每天通过定时任务同步一次就够用,不能走业务接口去高频更新。

4. 读写分离配置与实践:主从延迟和强制路由

4.1 主从数据源绑定

读写分离在分库分表基础之上叠一层,是这套工程的另一个亮点。分库分表解决的是数据量大的问题,读写分离解决的是单库连接数过高和高并发读的压力。工程里每个分片都配了一个主库和从库,配置结构不再是简单的两个数据源,而是每个逻辑库配一组主从节点。

rules: readwrite-splitting: >public OrderDO queryNewlyCreatedOrder(Long orderId) { try (HintManager hintManager = HintManager.getInstance()) { hintManager.setWriteRouteOnly(); return orderMapper.selectByOrderId(orderId); } }

setWriteRouteOnly()的作用范围是当前线程,到方法退出就结束了,不会影响上下文里的其他请求。这样刚下单完成的跳转页就能直接走主库去实时查询订单状态,而历史订单查询走从库降低主库压力。

4.3 事务内读操作全部路由到主库

还有一类容易被忽略的坑:SHOW 事务里如果执行了写操作,事务内的读只能走主库。ShardingSphere 的规则是,一旦事务里出现写操作,后续所有 SQL 都只能路由到写数据源。如果业务逻辑是“先更新一段内容,再查询更新后的内容”,而框架又强制把读路由到从库,那事务看到的还是旧数据。但这套工程的做法是直接在读操作处加@Transactional,让事务自身保证一致性:

@Transactional(rollbackFor = Exception.class) public OrderDO updateAndGet(Long orderId, BigDecimal amount) { orderMapper.updateAmount(orderId, amount); return orderMapper.selectByOrderId(orderId); }

这种写法下,ShardingSphere 的事务生命周期会保持同一个连接,读和写都在主库上完成,天然规避了读写分离下的主从延迟问题。代价是这个事务内不能做太重的查询,否则会长时间占用主库连接。

5. 分布式事务与 MyBatis 集成的四个高频坑

5.1 大事务导致连接池耗尽

分库分表之后的@Transactional和单库时代完全是两个概念。单库事务只占用一个连接,而分库分表下的事务可能横跨两个库,每个库占用一个连接。假设把 1000 个并发请求的事务都打在ds0、ds1上,每个请求同时占用两个连接,Druid 连接池默认 20 个连接,一下子就被打满了。所以很多团队在上了分库分表之后,把事务范围尽量缩小,只把必要的写操作放进事务,读操作全部挪出事务块。

连接池参数也要跟着调整。工程里给的 Druid 配置一般会把max-active调大,同时开test-while-idle保障连接可用性:

ds0: type: com.alibaba.druid.pool.DruidDataSource # ...连接信息... min-idle: 5 max-active: 50 test-while-idle: true time-between-eviction-runs-millis: 60000

日志里如果频繁出现wait millis 10000, active 50, maxActive 50之类的连接池满告警,先检查是不是事务范围太大,再调连接池大小。

5.2 分页查询翻车现场:深翻页与内存归并

MyBatis 的PageHelper插件在分库分表下有一个经典问题:它是在应用层拦截 SQL 并改写,但 ShardingSphere 接管了 SQL 路由之后,分页逻辑的处理方式发生了改变。假设每张物理表有 10 万条数据,客户端请求第 1000 页,每页 10 条。ShardingSphere 会把LIMIT 10000, 10改写成每个分片各自取LIMIT 10000, 10,然后在内存里汇总 20010 条再做归并。数据量一大,这个归并过程就会非常吃力。

工程里给出的建议是:对于深度分页场景,不要用传统的OFFSET翻页,尽量改成“上一页最后一条记录的 ID 或时间”来做位置游标。例如订单列表的下拉加载,传入lastOrderId,SQL 写成WHERE order_id < #{lastOrderId} ORDER BY order_id DESC LIMIT 10。这种写法每个分片只需取出 10 条,再在内存中归并,性能稳定。用我习惯的说法,这就是用“业务逻辑换取性能”。

5.3 分布式事务的不可能三角

ShardingSphere-JDBC 本身提供的是弱 XA 事务,并不等同于强一制性事务。默认的@Transactional在跨库场景下,只是尽力把多个写操作放在同一个本地事务里,并不能保证多个库之间的事务同时提交或回滚。它的XaTransactionManager和SeataATHandler需要额外引入依赖并配置,一般在工程里默认是关闭的。

事务方案的选型建议要结合业务场景来挑:

场景推荐方案原因
单库内多表写本地事务无需额外依赖,性能最高
跨库但不跨服务两阶段提交(XA)事务管理器协调,实现简单
跨服务、跨库Seata AT 模式最终一致,性能优于 XA
纯异步、可补偿本地消息表/事务消息不阻塞链路,适合业务解耦

工程本身只是纯 Servlet 单应用,所以本地事务已经够用。如果后面拆微服务了,再把 Seata 集成进来。

5.4 MyBatis 二级缓存与分片键的冲突

MyBatis 的二级缓存是全局级的,缓存的 key 是 SQL 语句加参数。但分库分表下,同一条 SQL 因为user_id不同可能路由到不同物理表,如果二级缓存没失效,就会发生“一个库里查到的数据被另一个库的请求复用”的严重问题。工程里默认把二级缓存关掉了,只在 Service 层做手工缓存,key 里带上user_id和dbIndex,例如:

public OrderDO getCachedOrder(Long userId, Long orderId) { String cacheKey = userId + ":" + orderId; OrderDO order = cacheService.get(cacheKey); if (order == null) { order = orderMapper.selectByUserAndOrderId(userId, orderId); cacheService.set(cacheKey, order, 60); } return order; }

分片键user_id必须出现在 SQL 的 WHERE 条件里。如果只传order_id去查,ShardingSphere 无法确定路由到哪个分片,只能全库全表扫描。慢查询日志里一旦大量出现这种“泛查询”,基本可以断定是业务代码漏了分片键。

6. 避坑与常见问题排查:从日志定位到后悔药

写分库分表代码,个人的感受是“坑多到怀疑人生”。这里整理出四条最具代表性的踩坑记录,基本能覆盖常见翻车现场。

6.1 启动报错:数据源循环依赖

现象:Spring Boot 启动时报The dependencies of some of the beans in the application context form a cycle,或者DataSource相关的循环依赖。

原因:工程里自己实现了一个自定义DataSource配置类,但 ShardingSphere 的 starter 也在装配数据源,两个 Bean 互相引用。大多是在还没看工程结构时,看到有 Druid 配置就顺手写了个@Configuration建DataSourceBean。

解决:把自定义DataSource类删掉或者加@ConditionalOnMissingBean,让 ShardingSphere 完全接管数据源创建。工程里如果不需要额外配置,最好只在application.yml里配,不要在代码里手动建DataSource。

6.2 数据一直只写入同一个库

现象:跑了几万条测试数据,发现ds0的某个表数据量特别大,ds1几乎没数据。

原因:分片算法选了user_id取模,但测试数据的user_id全部集中在偶数区间,或者配置的分片算法是MOD但实际没生效,走了默认的单库策略。最隐蔽的一种:使用了 ORDER BYcreate_time之类的排序时,ShardingSphere 归并结果只保证单个分片内有序,跨分片归并后排序是乱的,导致看着像数据分布不均。

解决:先查日志里 SQL 的路由结果。ShardingSphere 在debug级别会输出Actual SQL: ds0 ::: select ...,看这个就知道 SQL 实际发到了哪些分片。如果路由都在一个库上,多半是配置不生效或分片键没进 Where 条件。

6.3 主键重复导致插入失败

现象:应用启动时报主键重复错误,但看代码用的是 SNOWFLAKE 算法;或者数据插入到了错误的表。

原因:worker-id没有按实例区分,多节点部署后生成相同串号。另一个可能是主键字段在 MyBatis XML 里没有配置useGeneratedKeys,而是业务自己往实体里塞了一个固定值,直接把雪花 ID 覆盖掉了。

解决:检查每个应用实例的worker-id配置,并确保 MyBatis 的插入语句里使用数据库自动生成主键。工程里推荐的做法是实体主键上加@TableId(type = IdType.ASSIGN_ID),让 MyBatis-Plus 来生成长整型雪花 ID,就不再担心重复了。

6.4 分页查询结果数量和总数对不上

现象:PageHelper分页查总数时,返回的 count 是物理表的单表总数,不是全分片总和,或者分页结果重复。

原因:ShardingSphere 对SELECT COUNT(*)语句的解析是合并各分片的 count,但如果业务表有绑定表关系,却把绑定关系配漏了,JOIN 查询会变成全库笛卡尔积,count 结果自然不准确。

解决:在分片规则里把存在关联关系的表都配进binding-tables,让框架知道这两张表按同一维度切分。还有一种配置是broadcast-tables和binding-tables混合使用时,注意不要重复声明同一张表,不然启动时配置校验会报冲突。

7. 验证与调优:压测方法和三个进阶配置习惯

7.1 压测前改换路由的两种验证方式

上线前最稳妥的验证办法不是直接看业务是否正常,而是先在框架层面确认路由是否正确。ShardingSphere 的 SQL 解析日志默认在 debug 级别,启动类里把日志级别调低:

logging: level: org.apache.shardingsphere: debug

然后跑几个典型场景:按user_id查订单、按order_no查订单、多表 JOIN 查订单明细。观察控制台输出的 Actual SQL 是否精确命中单个分片,同时检查一个最容易出问题的点:SQL 里的表名是否被改写成了物理表名。

我自己有一个强制验证的习惯:写一个单测,插入 100 条user_id从 1 到 100 的订单数据,然后查每个物理分片的 count。如果两个库各约 50 条,且两个表也均匀分布,取模路由基本就是对的。这一步能比查日志更快暴露分片不均匀的问题,也方便后续调整算法。

7.2 参数调优参考

压测过程中重点关注三个指标:CPU、连接池占用、慢 SQL 数量。连接池参数建议按下面的基础值起步,再根据压测结果微调:

参数起步值说明
max-active50连接池上限,不宜超过数据库最大连接数
min-idle5最小空闲连接数,避免频繁建连
max-wait-millis10000获取连接超时时间,过大容易拖垮接口
time-between-eviction-runs-millis60000空闲连接检测间隔
maxPoolPreparedStatementPerConnectionSize20预编译语句缓存,提升高频 SQL 性能

超过 200 并发后如果出现大量连接等待,优先检查业务代码里是否存在慢事务,不要盲目加大max-active,它有可能会拖垮数据库服务器。

7.3 一个值得长期养成的配置习惯

工程里把分片规则全部放在了application.yml,这个在单环境够用,一旦多环境部署(日常、预发、生产),分片规则完全一致但物理库地址不同,全部堆在一个文件里就不太好维护。建议按环境拆分配置,把数据源地址放到application-{env}.yml中,把分片规则保留在application.yml,用spring.profiles.include引入。

另一个习惯是给每个物理库加一个逻辑前缀,例如ds_order_0、ds_order_1。这样在多套分片规则同时存在时(比如订单和用户分片维度不同),从日志里能一眼看出 SQL 路由到了哪套分片。否则看到ds0、ds1,根本分不清是订单分片还是用户分片。

7.4 踩过多次之后的复盘

每次排查分库分表问题,我都会先看 SQL 路由日志,再看连接池状态,最后才看代码逻辑。顺序反了往往会白白浪费半天。后来养成了一个习惯:任何一张表的分片规则变更,都在测试环境强制走一遍“插入一条 → 按分片键查出来 → 按非分片键查一次(预期全表扫描)→ 观察日志路由”的流程,确认路由正常后再提交代码。这套springboot_shardingjdbc_mybatis工程帮我省掉了大量重复的环境搭建工作,也希望对你有所启发。分库分表不是玄学,把路由规则、事务边界和连接池参数这三件事想清楚,大部分问题都不会发生。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询