Spring Boot整合Sharding-JDBC与MyBatis实现订单分库分表实战

发布时间:2026/10/15 2:49:08
Spring Boot整合Sharding-JDBC与MyBatis实现订单分库分表实战 简介面向Java后端开发者的分库分表实战示例工程基于SpringBoot、ShardingJDBC与MyBatis整合解决高并发、大数据量下单个数据库容量与性能瓶颈给出可直接运行的分片方案。压缩包共15个文件、约61KB结构简洁5个Java源码负责核心逻辑与启动类2个XML用于MyBatis映射与配置1个YAML和1个properties定义SpringBoot及分片规则1个SQL脚本负责初始化分库分表结构另附Maven包装器与README等辅助文件。已有187人学习浏览。项目实际演示了ShardingJDBC的哈希取模、范围分片等规则配置结合MyBatis完成对多个分片数据源的透明访问并涵盖数据源动态路由、读写分离思路、本地事务处理、SpringBoot整合配置等关键环节。阅读这份示例可以快速理清分库分表的核心配置步骤与常见坑点结合代码注释能够直接上手适合作为学习参考、项目脚手架或二次开发底座。1. 一张千万级订单表把系统逼到了分库分表的路口当订单表行数涨到千万级单表索引即使完美命中大促期间的随机读和写入锁竞争也会让接口延迟一次一次往上跳。这时候最务实的动作不是换数据库也不一定上独立中间件而是用 springboot_shardingjdbc_mybatis 这套组合在应用内部把数据拆到多个库和表里数据库关心的拆分粒度有了MyBatis 的 Mapper 又几乎不用重写。下文面对的读者是已经会用 Spring Boot 和 MyBatis但还没真正上手过分库分表的人。不讲概念堆砌只讲怎么落地一个可运行的分片数据源以及哪些参数设错了会翻车。先说结论Sharding-JDBC 是 Apache ShardingSphere 的 JDBC 内核以 jar 包形式嵌在应用里不需要额外部署服务。它接管 DataSourceSQL 进入后先做解析路由再改写、执行和归并对 MyBatis 来说就像在用一台普通数据库。这个方案适合大部分单表已经超过千万行、读写比高、又不想引入独立中间件的团队。跟着后面的配置逐步做你能得到一个按用户分库、按订单分表的可运行工程。2. Sharding-JDBC 干了什么从路由到归并内嵌式分片的四个本质机制2.1 为什么选应用内嵌 Jar而不是单独部署中间件分库分表有两条主流路线。一条是部署独立的数据库中间件应用把 SQL 发给中间件由它解析后转发到后端数据库多了一个网络节点也引入了一个需要专门运维的组件。另一条就是 Sharding-JDBC 这种应用内嵌方案它在启动时把普通 DataSource 包装成分片数据源SQL 在应用进程内完成路由和改写不产生额外的 RTT也不存在中间件单点故障。我一般会把内嵌方案作为中小团队的第一选择。原因很直接它不需要额外部署、不需要迁移连接池、不需要维持一套中间件的高可用改动边界被限制在工程配置里。代价也很明确每个应用实例都要持有分片规则规则变更是代码发布不是改一个中心化配置。对大部分电商、SaaS 场景这个代价其实是可接受的。还有一层是业务兼容性。团队里只要有人熟悉 Spring Boot 的自动配置就能把 Sharding-JDBC 接进来。它的配置前缀是spring.shardingsphere底层数据源仍然是你自己选的 HikariCP、Druid只是被包了一层。项目里原有的 MyBatis 代码不需要理解分片细节这决定了落地成本的上限。2.2 SQL 从进入到返回解析、路由、改写、执行、归并一个 SQL 经过 Sharding-JDBC 时动作顺序大致是这样的。第一步是解析。它把 SQL 文本解析成语法树识别出逻辑表名、查询条件里出现的列名、参数值。比如SELECT * FROM t_order WHERE user_id 123 AND order_id 456解析阶段就会把t_order标记为逻辑表把user_id和order_id标记为可路由条件。第二步是路由。根据分片键的值和分片算法计算目标分片。如果按user_id决定库、按order_id决定表那这条 SQL 会先算出123 % 2 1定位到 ds1再用456 % 2 0定位到t_order_0。如果查询条件里没有分片键比如只按order_id或status查分片键无法推导它只能走全路由把 SQL 发给所有库表。第三步是改写。把逻辑表名t_order改写成物理表名t_order_0同时把分片条件的拼装方式按目标分片重写。多表关联、分页、聚合在这个阶段都需要填充分片信息。第四步是执行。多个分片的 SQL 并行发到各数据源这一步一般不会单独感知到但要注意分片数量直接决定连接数和并发度。最后一步是归并。各分片返回的结果不能简单拼在一起排序、聚合、分页、去重都需要在应用侧再做一次归并。举个例子SELECT COUNT(*)会被改写成每个分片各执行一次COUNT(*)然后再把多个结果加起来。这四类动作里最影响日常调优的是路由和归并。路由决定性能基线归并决定你在分页、排序 SQL 上能不能抄原来的写法。2.3 MyBatis 和 Sharding-JDBC 的协作边界在哪里MyBatis 只依赖一个 DataSource 来创建 SqlSessionFactory。Sharding-JDBC 的 starter 在 Spring 容器里会覆盖掉你原本定义的数据源让所有依赖 DataSource 的组件拿到分片代理。这带来三个协作边界需要注意。第一MyBatis Mapper 的 XML 里写的是逻辑表名不是物理表名。你不需要在 XML 里写t_order_0、t_order_1写了反而会让分片规则失效。第二MyBatis 的Transactional拿到的是代理连接上的事务它只能保证单分片上的本地事务。跨库事务要另配分布式事务。第三MyBatis 二级缓存是全局 Map缓存 key 里没有分片信息开启后容易在分片场景里读到脏数据。这些边界在后面避坑章节会逐一展开。3. Spring Boot 里把 Sharding-JDBC 和 MyBatis 串起来最小可运行工程3.1 Maven 依赖与版本搭配先搭一个最小可运行工程。项目基础是 Spring Boot Web 工程引入 MySQL 驱动、MyBatis starter、ShardingSphere JDBC starter。以某电商订单模块为例pom.xml 里的关键依赖如下dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.3.2/version /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency这里版本号我使用的是当时验证过的组合实际接入时建议对照自己 Spring Boot 的版本上下浮动。ShardingSphere 5.x 的配置模型和 4.x 有明显区别4.x 里很多配置项在 5.x 中被改到rules.sharding下网上大量老文章对不上以你本地实际拉到的依赖文档为准。3.2 数据源与分片规则配置一份能直接抄的 application.yml假设订单库拆成两个物理库 ds0、ds1每个库里订单表拆成t_order_0、t_order_1。库的分片键是user_id表的分片键是order_id取模基数都是 2。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/order_db_0?useSSLfalseserverTimezoneAsia/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/order_db_1?useSSLfalseserverTimezoneAsia/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: order_db_mod table-strategy: standard: sharding-column: order_id sharding-algorithm-name: order_table_mod key-generator: column: order_id type: SNOWFLAKE sharding-algorithms: order_db_mod: type: MOD props: sharding-count: 2 order_table_mod: type: MOD props: sharding-count: 2 props: sql-show: trueactual-data-nodes里的ds$-{0..1}是 Groovy 表达式等价于ds0,ds1。它声明了 t_order 的物理分布是分片的锁眼。database-strategy负责决定这条 SQL 去哪个库table-strategy决定去哪个表。sharding-algorithm-name引用的是下面sharding-algorithms里定义好的算法。MOD 表示取模sharding-count是分片数量也就是取模的基数。需要注意key-generator的配置。5.x 里如果给主键列配置了 SNOWFLAKE 生成器插入语句不传order_id也会自动生成。但在多实例部署时默认的 worker-id 可能重复后面避坑章节会讲。3.3 实体、Mapper 与 Service让现有代码无感分片新建一个t_order对应的实体类字段和表结构保持一致。public class OrderDO { private Long orderId; private Long userId; private java.math.BigDecimal amount; private Integer status; // 省略 getter/setter }Mapper 接口只要把表名写成逻辑表t_order就行Sharding-JDBC 的路由对应用是透明的。Mapper public interface OrderMapper { int insert(OrderDO order); OrderDO selectByUserIdAndOrderId(Param(userId) Long userId, Param(orderId) Long orderId); }对应的 XML 里仍然写t_order。查询条件必须同时带上user_id和order_id这样库和表两层路由都能命中单一分片否则会触发全路由。insert idinsert parameterTypeOrderDO INSERT INTO t_order (order_id, user_id, amount, status) VALUES (#{orderId}, #{userId}, #{amount}, #{status}) /insert select idselectByUserIdAndOrderId resultTypeOrderDO SELECT order_id, user_id, amount, status FROM t_order WHERE user_id #{userId} AND order_id #{orderId} /selectService 层不需要任何分片感知注入 Mapper 后正常写业务逻辑。Service public class OrderService { private final OrderMapper orderMapper; public OrderService(OrderMapper orderMapper) { this.orderMapper orderMapper; } public void createOrder(Long userId, Long orderId, java.math.BigDecimal amount) { OrderDO order new OrderDO(); order.setUserId(userId); order.setOrderId(orderId); order.setAmount(amount); order.setStatus(1); orderMapper.insert(order); } public OrderDO getOrder(Long userId, Long orderId) { return orderMapper.selectByUserIdAndOrderId(userId, orderId); } }3.4 验证插入两条数据后确认数据真的落到了不同分片在应用启动类里加一个临时的 CommandLineRunner插入两个 userId 奇偶不同的订单然后分别查询。Component public class DataInitRunner implements CommandLineRunner { private final OrderService orderService; public DataInitRunner(OrderService orderService) { this.orderService orderService; } Override public void run(String... args) { orderService.createOrder(10001L, 90001L, new java.math.BigDecimal(299.00)); orderService.createOrder(10002L, 90002L, new java.math.BigDecimal(199.00)); System.out.println(orderService.getOrder(10001L, 90001L)); System.out.println(orderService.getOrder(10002L, 90002L)); } }运行起来后打开sql-show控制台会打印真实的物理数据源和物理表名。如果看到order_db_1里插入t_order_1而order_db_0里插入t_order_0说明路由链路已经通了。这个验证比写单元测试更直观排查环境因素时也更快。4. 分片键与分片算法订单表按 user_id 分库、order_id 分表的参数拆解4.1 分片键选型从查询模式倒推而不是看哪列像主键最常见的分片键选型错误是把数据库主键直接当成表分片键。主键是雪花 ID分布均匀取模后数据看起来也很平衡但业务查询很少只按主键查。订单场景里用户查自己的订单一定是先带user_id运营后台按订单号查也许只带order_id。如果你的查询条件大多以user_id开头那库分片键就应该选user_id。我一般把分片键分成两类。一类是“路由主键”要求高频查询条件里必然携带且分布足够均匀比如user_id。另一类是“定位主键”用于精确锁定一条记录比如order_id。在订单场景里user_id决定数据落在哪个库order_id决定落在库里的哪张表。两者组合后单条订单查询可以在毫秒级定位到唯一分片。如果业务里存在大量不带分片键的管理查询比如“按状态查所有待支付订单”分片键怎么选都救不回来。常见做法是再维护一张索引表或者把查询请求引导到只读的汇总库而不是指望分片规则帮你全路由。4.2 MOD 与 HASH_MOD取模、哈希以及表达式参数配置里用的 MOD 算法就是value % sharding-count。它要求分片键是整数且分布均匀。如果分片键是字符串比如手机号、地区码直接用 MOD 会因为 ASCII 码取模导致严重偏斜。HASH_MOD 会先算字符串的哈希值再取模参数仍然是sharding-count。sharding-algorithms: user_db_mod: type: HASH_MOD props: sharding-count: 2actual-data-nodes表达式里$-{0..1}的语义是展开一个范围也可以用逗号枚举ds0,ds1.t_order_0,t_order_1。范围表达式的优点是改分片数时只动数字但要注意它和具体规则里sharding-count的匹配sharding-count是 2节点范围却是0..3那多出来的两个分片永远拿不到数据。还有一个容易被忽略的点库分片算法和表分片算法的sharding-count不一定相同。库只有 2 个表可以拆 8 张。表达式里ds$-{0..1}.t_order_$-{0..7}才是完整的表分布。很多人在这一步只改表数量、忘记同步tables.t_order的actual-data-nodes导致新表建好了但路由永远只走向老表。4.3 绑定表与广播表解决关联查询和字典表的路由错位分片后最麻烦的是表与表之间的关联。订单主表按order_id拆订单明细表如果按user_id拆两张表的分片键不一致join 时就会跨库。解决办法是让它们使用完全相同的分片键和分片策略然后配置成绑定表。rules: sharding: binding-tables: - t_order,t_order_item绑定表的意思是这两张表中分片键相同的记录被放到相同的物理分片。t_order和t_order_item都用order_id做表分片、都用user_id做库分片那么 join 时 Sharding-JDBC 可以确保关联数据在同一分片内完成避免笛卡尔积式的跨库关联。字典表如地区、状态枚举数据量小且会被所有分片引用这类表有两种处理方式。一是做成广播表配置里声明后插入、更新、删除会广播到所有分片每个库都保留一份完整数据。rules: sharding: broadcast-tables: - t_dict广播表的代价是写操作放大。N 个库就要执行 N 次事务边界也变得更宽。我通常只对真正只读、几乎不更新的表用广播其余配置表一律不放到分片库里放一个独立业务库让各分片通过普通 join 或远程调用去拿。这样能少踩很多写放大和事务的坑。5. 集成避坑记录五条让配置和 SQL 集体翻车的真实教训5.1 全路由导致慢查询WHERE 里少了 user_id瞬间打穿四个分片现象线上一个订单查询接口平时 20ms某天突然飙到 500ms。日志里出现几条一模一样的 SELECT只是表名分别是t_order_0到t_order_1两两组合等于一次查询把全部分片都扫了一遍。原因调用方只传了order_id没传user_id。库分片键user_id缺失Sharding-JDBC 无法定位库只能全路由。全路由本身不是 bug但它把并发压力放大了分片数倍慢查询就是这么来的。解决把user_id作为接口必传参数在 service 入口直接校验。如果确实有只按order_id查的场景单独建一个order_id - user_id的映射索引表先查映射再走分片查询。不要容忍不带库分片键的查询进到分片链路里。5.2 雪花 ID 冲突SNOWFLAKE 生成器在多实例下频繁主键冲突现象插入订单时偶发Duplicate entry xxx for key PRIMARY而且只在高并发、多实例部署时出现。原因默认雪花算法依赖 worker-id 区分不同实例。多个应用实例各自为政没有把 worker-id 错开导致同一毫秒内可能生成相同 ID。还有一种情况是宿主机时钟回拨也会让雪花 ID 重复。解决在key-generator里显式配置。key-generator: column: order_id type: SNOWFLAKE props: worker-id: 1多实例部署时给每个实例配置不同的worker-id。如果是容器环境优先使用环境变量注入避免写死在配置文件里。对时钟回拨敏感的系统更加稳妥的做法是业务层自己生成有序 ID把雪花的时钟依赖从根上去掉。5.3 深分页翻到后面几页LIMIT 偏移量被放大到整个结果集现象订单列表第 1 页正常翻到第 50 页时接口明显变慢再往后直接超时。原因分片后LIMIT 1000, 20并不是每个分片只查 20 条。Sharding-JDBC 为了保证最终结果正确会先让每个分片查出偏移量 页大小的记录再到内存里归并排序最后截取第 1000 条之后的 20 条。分片越多、偏移量越大归并成本线性上涨。解决不要放任深分页。列表页控制在 100 页以内超过后改用游标分页。游标分页的本质是用排序字段定位比如where create_time 上次最后一条的create_time order by create_time desc limit 20。虽然游标分页也会触发全路由但每次只取 20 条不会把前面 1000 条全捞回来做归并。5.4 跨库更新成功一半Transactional 并没有分布式事务能力现象一次操作同时更新订单表和库存表库存扣减成功订单状态没改事务回滚了但回滚不完整出现数据不一致。原因Transactional管的本地事务只能覆盖单个连接、单个分片。跨多个物理库的更新每个库的提交独立执行Spring 注解层面的回滚指令不可能让已经提交的另一个库再把数据改回去。解决按业务优先规避跨库事务。订单和库存如果必须强一致应该换分布式事务框架比如 Seata。如果允许最终一致就用本地消息表或事务消息去异步补偿。最容易踩的坑是以为加上Transactional就万事大吉实际上 Sharding-JDBC 默认模式下事务只能保证单分片原子性。5.5 MyBatis 二级缓存开启后读到脏数据缓存 Key 没有分片维度现象后台修改了一条订单状态前端再查部分用户读到的还是旧状态且重启应用后数据又变正常。原因MyBatis 二级缓存是全局命名空间级别的 Map。分片后同一个 Mapper 的缓存不区分数据来自哪个物理分片更新只刷新了执行分片的缓存其他分片的查询结果一旦被缓存命中返回的就是旧值。解决分片环境下关闭 MyBatis 二级缓存。mybatis: configuration: cache-enabled: false如果业务确实需要缓存不要在 MyBatis 层做在 Service 层用 Redis 或 Caffeine缓存 key 显式拼上分片键和分片路由信息。这样即使某个分片更新也能按 key 精准失效。6. 进阶读写分离与 Hint 路由把 Sharding-JDBC 的边界摸清楚数据量上来之后订单库的读压力通常会先于写压力爆掉。Sharding-JDBC 可以在分片规则之外再叠一层读写分离给每个分片库挂一个只读副本。配置思路是先用readwrite-splitting规则把 ds0、ds1 各自包装成读写数据源再让分片规则引用这个新的数据源名。spring: shardingsphere: rules: readwrite-splitting: >try (HintManager hintManager HintManager.getInstance()) { hintManager.setWriteRouteOnly(); orderMapper.updateStatusByOrderId(orderId, newStatus); }这套组合我在订单系统里跑了两年最大的感受是配置不难难的是把“哪些查询必须带分片键”“哪些表不能 join”“哪些事务允许跨库”定成团队规范。技术边界可以由框架兜住业务边界只能靠人来守住。分库分表给了系统扩容的空间也给了写代码时更多的约束。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询