Spring Boot整合ShardingSphere-JDBC分库分表实战:从配置到踩坑全解析
简介一套基于SpringBoot、ShardingJDBC与MyBatis的分库分表示例工程面向需要掌握数据库水平拆分与分布式数据访问的Java后端开发者可直接作为学习支架或项目脚手架。工程覆盖哈希取模、范围分片等常见策略配置演示多数据源动态路由、Mapper映射绑定并说明本地事务与分布式事务的取舍场景同时配套SQL初始化脚本和简明说明文档导入IDE后即可运行并观察分片路由结果。压缩包共15个文件其中5个Java源码对应核心逻辑2个XML为MyBatis映射1个YAML定义分片规则另有SQL脚本、Maven Wrapper及properties等辅助配置整体仅61KB目录结构清晰。从配置到编码再到调试保留了最小可运行闭环尤其适合在真实业务改造前快速验证分片方案。目前已有185人学习下载适合希望快速理解ShardingJDBC核心机制、参照集成细节并迁移到实际项目的开发者。 Spring Boot整合ShardingSphere-JDBC那套事我前后在两个团队里搭过两遍一次是订单表千万级数据撑不住了一次是用户行为日志入库把MySQL写入打满每次都是踩着坑过来的。这个标题组合“springboot_shardingjdbc_mybatis”看起来就是典型的分库分表改造场景网上资料要么只讲配置不讲原理要么直接拿官方文档糊弄人。这篇我按真实项目落地的思路来聊从为什么非改不可到一步步怎么拆、怎么配、怎么写代码再到后来线上踩过的那些坑一次说透。先说清楚这套组合是干什么的。Spring Boot是底座负责把整个应用跑起来MyBatis是ORM层负责Java对象和数据库记录之间的映射ShardingSphere-JDBC热词里写的shardingjdbc就是它是中间那一层以jar包形式嵌入应用拦截SQL、改写路由、聚合结果。通俗点说ShardingJDBC相当于给MyBatis配了一个“智能分发表”你的代码还是写SELECT * FROM t_order WHERE order_id ?但底层到底查哪张表、哪台库ShardingJDBC引擎会自动帮你把SQL改写掉应用层完全无感。这套方案适合谁来参考就是那些单表数据量到了几百万、上千万读写延迟明显变高或者单库连接数被耗尽的团队。以及准备做技术方案选型、面试准备分布式数据架构的Java开发都可以从这篇里拿走可直接落地的方案。要是数据量还小、单库单表活得好好的我不建议你上这套东西它本质上是给系统续命的提前引入只会徒增复杂度。1. 整体设计与技术选型为什么非得是ShardingJDBC1.1 分库分表不是炫技是数据量逼到墙角了我先讲一个比较典型的例子。我们当时订单表单表一天新增近20万条上线两年多接近7000万行。MySQL的B树再牛单表数据量过千万之后索引深度加深、缓冲池命中率下降、写入产生的大量随机IO都会让核心链路响应时间肉眼可见地上升。再加上我们主库还承担了实时统计查询一条带多个GROUP BY的报表SQL就能把CPU打到60%以上业务方天天投诉。当时摆在面前的路有三条缓存前置、归档冷数据、分库分表。缓存前置只能缓解读压力对写放大和单表体积没帮助归档是把老数据挪走但业务上要求订单至少要保留三到五年可查这条也堵死了。算来算去只能上分库分表把压力和流量打散到多张表、多个库里去。这个过程中我们对比过ShardingJDBC和ShardingProxy同一个ShardingSphere生态下的两种形态也对比过MyCat。ShardingJDBC是个嵌入式驱动直接和你的应用跑在一个进程里配置简单、性能损耗极小——它对SQL的改写和路由都在本地完成不经过任何外部代理节点很适合我们这种Java单体应用改造。MyCat走的是中间代理层独立部署但运维成本和网络开销更高而且对分布式事务的支持没有ShardingSphere这套好。ShardingProxy虽然也是生态里的但适用场景更偏向多语言接入和跨系统分片我们一个纯Java项目没必要多养一个中间件。1.2 分片键选型拍脑袋选错后面全得返工分库分表方案里最重要的不是中间件是分片键的选定。分片键决定了数据往哪个库、哪张表里落。我们订单场景用的是哈希取模分片分片键选的是用户ID不是订单ID也不是订单创建时间。原因不复杂用户维度的查询是订单系统最高频的访问路径WHERE user_id ?这类SQL占了70%以上。这里有个很关键的取舍逻辑——如果用订单ID作为分片键按用户查订单时你就必须全库全表扫描因为一个用户的订单会散落在各个分片里中间件再聪明也不可能在未知订单ID的前提下迅速定位数据。反过来用用户ID做分片键按订单ID查的SQL虽然也需要全路由但这类SQL业务上不多偶尔扫一遍全分片也扛得住。那为什么不用订单创建时间时间分片容易产生热点比如大促日一天的数据把当天分片打满其他分片闲着这轮改造的目的就是把单点压力抹平时间字段显然做不到。所以综合评估下来选用户ID取模分表用户量均匀、查询路径顺畅、以后扩容也好按用户基数规划。1.3 数据节点规划分几库、分几表是有讲究的规划分片时我们直接定了4库×4表也就是16个数据节点。这个数怎么算出来的先看业务天花板——按当时增速未来三年订单总量预计在3亿行左右单张分表控制在500万行以下是MySQL性能和运维都舒服的区间3亿除以500万等于60张表为了将来扩容余量我们直接按2的幂次方设计16个节点不够再翻倍到32、64因为ShardingJDBC取模分片最适合的扩容方式就是分片数翻倍。下面这个表格是我们压测后的实际结论。16节点和4节点性能差异非常大但16节点继续往32节点走收益就没那么明显了所以规划的时候把“够用”和“别过度设计”平衡好。分片规模数据总量预估单分片数据量查询耗时压测均值写入吞吐压测值4节点2库×2表5000万1250万46ms3200 TPS8节点2库×4表1亿1250万41ms3300 TPS16节点4库×4表3亿1875万18ms6100 TPS32节点4库×8表5亿1562万15ms6400 TPS实际数据告诉我们只要单分片数据量控制得当16节点和32节点在性能上没有本质差别但节点越多连接管理、SQL聚合、分布式事务的成本都会上涨。不要盲目追求分得多够用就行。2. 环境版本与依赖配置Spring Boot整合ShardingJDBC最容易踩的坑2.1 版本兼容性这一步错了后面全白搭先说最让人头大的版本兼容问题。ShardingJDBC的版本演进比较大4.x和5.x在配置风格上有质的区别——4.x的配置走spring.shardingsphere.config前缀5.x统一改成spring.shardingsphere.rules前缀。你要是直接抄老博客的配置粘贴到新项目里启动直接报Configuration error: Cant find schema之类的一大串异常而且你还会发现IDE根本不给你提示排查起来特别消耗耐心。我自己用的组合是Spring Boot 2.7.x ShardingSphere-JDBC 5.3.2 MyBatis Spring Boot Starter 2.3.x MySQL 8.0。这个组合是经过我验证比较稳定的搭配。Spring Boot 3.x用的是Jakarta EE规范和新的AOT编译机制ShardingSphere对Spring Boot 3的支持到5.4后才逐渐成熟如果你项目刚起步直接用Spring Boot 3也没问题但务必配套ShardingSphere 5.4别用老版本去硬扛。跟Spring Boot版本同样重要的还有MyBatis的整合细节。热词里搜到的“springboot整合mybatis”、“idea创建springboot项目”这些我都翻过很多案例为了方便只创建一个数据库就完事了但分库分表场景下必须搞清楚一个概念ShardingJDBC逻辑库和物理库的对应关系。你配置里写的那个逻辑库名业务代码直接连它底层真正的物理库是你在dataSources里定义的那几个真实JDBC连接地址。另外很多人不知道Spring Boot工程里加入ShardingJDBC之后它默认会用自己的数据源包装所有DataSource组件如果你在代码里还有手动Bean创建的DataSource两边会打架。所以配置分库分表后不要再手动定义数据源Bean让框架全权接管除非你想手动指定Primary去压过它。2.2 一步一步配置YAML逻辑表、分片算法、绑定表如果你也是用application.yml来配置我下面贴一份调整过注释的完整配置可以直接对照着用spring: shardingsphere: datasource: names: ds0, ds1, ds2, ds3 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.10:3306/order_db_0?useSSLfalseserverTimezoneAsia/Shanghai username: order_app password: xxx # ds1、ds2、ds3 以此类推改 IP、端口、库名即可 rules: sharding: tables: t_order: actual-data-nodes: ds$-{0..3}.t_order_$-{0..3} database-strategy: standard: sharding-column: user_id sharding-algorithm-name: db_hash_mod table-strategy: standard: sharding-column: user_id sharding-algorithm-name: table_hash_mod key-generate-strategy: column: order_id key-generator-name: snowflake_id binding-tables: - t_order, t_order_item sharding-algorithms: db_hash_mod: type: HASH_MOD props: sharding-count: 4 table_hash_mod: type: HASH_MOD props: sharding-count: 4 key-generators: snowflake_id: type: SNOWFLAKE props: worker-id: 1 props: sql-show: true这段配置里藏着几个值得重点说明的细节。actual-data-nodes: ds$-{0..3}.t_order_$-{0..3}的意思是每个库下面都有一张同名的逻辑表对应物理表总共4×416张。这里$-{0..3}是ShardingJDBC的行内表达式语法它支持遍历生成字符串也支持逗号分隔枚举比如$-{0,2}就指0和2两个节点。分片算法用的是HASH_MOD就是哈希取模。根据user_id的哈希值除以分片数量得到的余数决定数据落到哪个库、哪张表。因为库和表的数量都是4所以同一个user_id的订单它所在的库号和对用户ID取模后的表号不一定是同一个数字但一定是可推导的。这里有个坑如果分库键和分表键不一致比如库用user_id、表用order_id那你写入和查询时必须同时满足这两个分片键条件否则路由就会退化成全路由数据量一大性能就会崩。binding-tables这个配置我重点说一下。它能把逻辑上存在关联关系的表比如订单表和订单明细表绑定在一起保证它们分片规则一致、数据始终落同一分片这样t_order JOIN t_order_item的SQL就不会产生笛卡尔积。如果不配绑定表一个订单JOIN明细ShardingJDBC会把两张表任意配对路由一次JOIN查询可能产生16次物理查询数据库直接被拖垮。2.3 依赖引入顺序MyBatis和ShardingJDBC的先后影响Maven依赖的顺序也会踩坑。MyBatis的sqlSessionFactory在创建时是抱着名为dataSource的Bean去初始化的如果ShardingJDBC的配置加载晚了MyBatis拿到的还是原始的单库数据源分片逻辑根本没生效。所以pom中ShardingJDBC的依赖要放在MyBatis Starter之前或者至少让它被Spring Boot自动配置优先处理。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实际工作中我还遇过一种情况本地开发IDE用的是内嵌Tomcat线程池应用里有连接池预热逻辑ShardingJDBC初始化的多个数据源连接池同时建立连接导致启动时数据库连接数爆掉。这个不是依赖顺序的锅是连接池initialSize配太大了后面我会在排错章节专门讲。3. 核心功能落地分片策略、分布式主键与读写分离3.1 自定义分片算法取模之外还有更贴合业务的玩法内置的HASH_MOD适合均匀分布的数据但如果你遇到一些数据分布天然倾斜的场景就要写自定义算法了。比如我们另一个系统里按机构维度分片大型机构的用户量是小型机构的几十倍哈希取模会把大机构的数据全部压到少数几个分片照样会热点。ShardingJDBC的StandardShardingAlgorithm接口允许你自定义精确分片和范围分片两个方法。我写过一版按机构级别加权的算法先在分片键里把机构类型编码进去再用“机构编码加权基数”做哈希这样大机构自然拥有更大的哈希空间数据能更加合理分散。public class OrgAwareShardingAlgorithm implements StandardShardingAlgorithmString { private static final int SHARDING_COUNT 16; Override public String doSharding(CollectionString availableTargetNames, PreciseShardingValueString shardingValue) { String orgCode shardingValue.getValue(); // 机构编码规则: 前两位是机构类型, 大机构权重更高 int weight resolveOrgWeight(orgCode); int targetIndex (orgCode.hashCode() * weight) % SHARDING_COUNT; return availableTargetNames.stream() .filter(name - name.endsWith(String.valueOf(targetIndex))) .findFirst().orElseThrow(() - new UnsupportedOperationException(无法定位分片)); } }这类自定义算法能从代码层面解决热点问题但前提是你得对你的业务数据分布有清醒的认知。我建议先用SQL把现有数据的user_id/org_code分布导出来看一眼确认偏斜程度再决定要不要上加权方案。3.2 分布式主键别再用数据库自增ID了分库分表之后数据库自增ID有一个先天缺陷不同分片各自从1开始全局一定会撞ID。所以分布式主键是必选项。ShardingJDBC内置雪花算法Snowflake配置里写key-generator-name: snowflake_id后在你执行insert时它会自动往配置的主键列填充一个全局唯一ID。这段逻辑是透明的MyBatis的insert语句不需要做任何改动你不需要主动给order_id赋值框架会生成好后再把ID回填到实体对象的对应字段里。这里有个坑接上文一定要在实体类的主键字段上加TableId(type IdType.INPUT)如果你用的是MyBatis-Plus否则MyBatis-Plus自带的ID策略会覆盖掉你雪花算法生成的值。如果用原版MyBatis只要insert语句里不包含主键列就没事ShardingJDBC会自己填。但如果你用了useGeneratedKeystrue keyPropertyorder_id要小心框架之间对“谁生成主键”的优先级处理经常会出现生成的ID还是0的情况。雪花算法本身也有个容易被忽视的参数worker-id。这个值用来区分不同实例生成ID时的机器ID如果多实例部署又没有正确配置高并发下不同机器会产生重复ID。worker-id取值范围0~1023有多少台实例就配多少个不同的值最好直接写到环境变量里不要写死在配置文件。3.3 读写分离与分片规则共存一套配置同时搞定很多系统的查询量远大于写入量分表的同时也会想顺带把读写分离做了。ShardingJDBC 5.x可以在同一个rules节点下同时配置分片和读写分离规则但要注意它的优先级读写分离作用于数据源层分片作用于逻辑表层它们可以叠加使用。rules: readwrite-splitting: >select idselectByUser resultTypeOrder SELECT * FROM t_order WHERE user_id #{userId} AND status IN (PAID, SHIPPED) /select这个SQL带user_id分片键按理说路由没问题。但MyBatis开启二级缓存之后缓存的Key只包含SQL语句和参数值不包含分片信息。当同一SQL被不同user_id的数据命中同一缓存区域时就会出现读到别人数据或者读不到刚插入数据的情况。这个问题的解法也很简单分库分表环境下直接关闭MyBatis二级缓存mybatis.configuration.cache-enabledfalse一级缓存作用范围限定在session内对跨分片查询来说最安全。5. 线上运维与后续演进扩容时值得提前做的几件事改造完成上线只是开始后续维护和扩容才是真正考验架构设计的地方。按我们的经验有三件事建议提前准备。第一件是提前规划好监控大盘。别等线上出问题再想怎么监控分库分表后核心指标除了常规的JVM、QPS、RT一定还要加数据源连接池活跃数、各分片数据量增长曲线、各分片慢SQL数量和耗时分布、分布式事务失败次数。这些指标能让你在分片继续膨胀之前就发现风险。第二件是预演扩容流程。ShardingJDBC的取模分片一旦定下来扩容就不是简单加节点的事因为取模基数变了存量数据必须重新路由。我们当时为了避免停服做了双写迁移先同时写旧分片和新分片然后跑历史数据迁移校验脚本最后读流量灰度切到新分片全部确认无误后关闭旧分片写入。这套流程前前后后折腾了几个版本才稳定提前演练能帮你把风险压低。第三件是考虑是否从ShardingJDBC迁移到ShardingSphere-Proxy。虽然我们Java单体用的是嵌入式驱动但公司后来接触了几支非Java技术栈的团队他们的服务也想来访问分片数据嵌入式方案就没法跨语言了。这种情况ShardingProxy作为独立的数据库代理服务更适合做统一接入层它可以只部署一个proxy对上提供服务底下复用同一套分片规则技术上完全兼容。从我个人的实际操作体会来说分库分表真正的难度不在配置和代码而在数据分布意识和一致性思维。只要多花点时间把分片键选好、把写入查询路径的分布画清楚、把跨分片查询的边界划出来后续大部分事故都是可以提前预判的。如果你正准备动这套技术方案建议一步步来先拿出一两个非核心表做试点跑通全链路再逐步铺开别梦想着一夜之间把核心交易库全部改造完毕那样大概率会把自己和团队都拖进泥潭。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →