ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ShardingSphere-JDBC 5.5.0整合Spring Boot分库分表全流程实战与踩坑指南

ShardingSphere-JDBC 5.5.0整合Spring Boot分库分表全流程实战与踩坑指南 分库分表这件事很多团队是走到“不得不做”的阶段才回头看中间件选型。我最近在重构一个订单服务单库数据量跑到几千万行之后再华丽的索引也挡不住毛刺最后决定用Apache ShardingSphere的方式落地。整套方案里最基础也最关键的环节就是ShardingSphere-JDBC 5.5.0和Spring Boot的集成这一步没走稳后面的分片策略、读写分离全是空中楼阁。这篇记录我在基础配置里实际的操作过程、参数取舍和踩坑记录给准备从0到1接入的同学一份可以直接抄作业的清单同时也聊聊版本选型、工程搭建、核心配置和故障排查这几件事的来龙去脉。1. 为什么是ShardingSphere-JDBC 5.5.0选型与前置认知1.1 客户端模式与代理模式怎么选更合适ShardingSphere有两个形态一个是部署在业务应用里的客户端模式ShardingSphere-JDBC另一个是独立部署的代理模式ShardingSphere-Proxy。很多第一次接触分库分表的同学会在这两个形态上纠结很久我直接说结论如果你的业务代码是Java技术栈项目又跑在Spring Boot这类框架里ShardingSphere-JDBC是更顺手的选择因为它以jar包形式嵌入应用连接的是业务自己的数据源没有额外的网络转发层延迟低运维上也少一套独立服务。ShardingSphere-Proxy则更适合异构语言接入比如PHP、Go这些不好直接依赖Java SDK的场景。但Proxy是独立服务所有SQL要先经过它再转发到后端数据库链路更长性能损耗也更高。我在实际中见过有人明明全是Java服务却非要在前面搭一个Proxy做统一入口结果排查问题时多了一层链路定位慢了不少。所以技术选型除非有明确的异构接入需求否则Java应用优先考虑JDBC模式。1.2 5.5.0版本的变化点和兼容性边界5.5.0是ShardingSphere 5.x系列里比较重要的版本它的配置模型已经从早期那种“读写分离”加“分片”混杂的旧写法彻底转向了“规则驱动”的模式。也就是说所有能力都抽象成各种规则比如分片规则、读写分离规则、数据加密规则在配置里以rules节点统一组织。好处是配置结构干净改一个模块不会让其他模块的配置变得不可读坏处是网上很多老教程还是4.x的写法照抄会直接启动报错。这个版本对Spring Boot 2.x和Spring Boot 3.x都有兼容适配但要注意JDK版本。Spring Boot 3.x默认要求JDK 17及以上所以如果你的项目还在JDK 8就老老实实待在Spring Boot 2.7.x的版本线附近配5.5.0的ShardingSphere-JDBC没问题。我的实际经验是JDK 17配合Spring Boot 3.2.x加ShardingSphere-JDBC 5.5.0这套组合在绝大多数业务场景下都非常稳定。1.3 前置环境准备接入之前先确认几个最基础的环境项一是数据库我这边用的是MySQL 8.0建议至少5.7以上因为较老版本的MySQL在分页、窗口函数、JSON支持上会有不少SQL兼容性问题二是需要准备好分片后的物理库和物理表ShardingSphere-JDBC并不会自动建库建表这点很多人第一次用会踩坑三是Maven工程确保可以拉取外部依赖。环境清单我列一下JDK 17Spring Boot 3.2.5ShardingSphere-JDBC 5.5.0MySQL 8.0.36HikariCPSpring Boot默认自带MyBatis-Plus 3.5.5。这套组合我在本地和测试环境都跑得很熟后面的配置示例也全部基于这套环境来写。2. 工程搭建依赖引入与基础改造2.1 Maven依赖到底怎么加ShardingSphere-JDBC 5.5.0的Spring Boot接入方式很简单核心依赖就一个shardingsphere-jdbc-core-spring-boot-starter它会把数据源、事务、规则解析全部装配进去。我见过有人在网上旧帖子里抄了一堆shardingsphere-core、shardingsphere-jdbc这种细粒度依赖结果版本冲突折腾半天。实际上5.x之后官方推荐的做法就是引入这个starter其他模块会按需传递依赖。dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.5.0/version /dependency如果项目里用到MyBatis或者MyBatis-Plus还要额外引入对应依赖。以MyBatis-Plus为例因为ShardingSphere-JDBC并不会替代你的ORM框架它只是把数据源包装成了ShardingSphereDataSourceMyBatis仍然在这个包装后的数据源上工作。dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency2.2 启动类与数据源覆盖的问题接入ShardingSphere-JDBC之后Spring Boot原本基于application.yml自动配置的DataSource会被ShardingSphere的配置覆盖。这个覆盖动作在5.x里是可控的但你需要显式允许Bean定义覆盖否则应用启动时会因为DataSource类型的冲突直接报错。在Spring Boot 3.x里我需要在application.yml中加这么一段配置spring: main: allow-bean-definition-overriding: true很多第一次接入的同学看到这个配置第一反应是担心它会不会影响其他Bean的覆盖行为。实际上Spring Boot默认只允许同名的Bean定义被覆盖allow-bean-definition-overriding就是放宽这个限制。ShardingSphere在装配数据源时会把原始数据源的BeanDefinition替换成自己生成的ShardingSphereDataSource所以必须开启覆盖。如果不开启动时大概率会遇到类似BeanDefinitionOverrideException或者数据源类型不匹配的问题排查起来很迷惑。2.3 启动验证与日志观察工程依赖加好、配置改成允许覆盖之后先不要急着写分片算法直接启动一次应用看日志是否出现ShardingSphere的初始化信息。正常情况下日志里会打印当前加载的数据源名称列表、规则类型和分片算法信息。如果这里就是一片空白那大概率是配置没有生效常见的原因是spring.shardingsphere这个配置前缀没写对或者已经按旧版写法配置了sharding.jdbc.config这种前缀。我把启动日志里比较关键的一行贴出来[INFO ] ShardingSphere startup successfully. ......看到这行日志说明ShardingSphere-JDBC已经成功注入了数据源下一步就可以开始写分片规则了。3. 核心配置拆解数据源、分片规则与分布式ID3.1 多数据源配置注意jdbc-url和driver-class-name在ShardingSphere-JDBC里你将面对的不再是单调的单数据源配置而是要配置多个物理数据源并且给每个数据源起一个逻辑名称。配置结构在5.5.0版本中是这样的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_ds0?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 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_ds1?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: root我特别提醒一下在数据源配置里连接地址的键名是jdbc-url不是url。这点很多人容易踩坑因为Spring Boot原生的HikariCP配置用的通常是jdbc-url但如果你习惯了Druid的url写法很容易在这里写错。写错之后应用启动时会提示找不到数据源属性但报错信息并不直观需要仔细排查。3.2 订单表分片规则逐步配置分片规则是整个配置的核心。我以订单表t_order为例做一个两库两表的分片方案即逻辑表t_order会按照订单ID取模拆到t_order_0和t_order_1两张物理表并且这两张表分别分布在ds0和ds1两个库里。这样实际数据节点就是四个物理表ds0.t_order_0、ds0.t_order_1、ds1.t_order_0、ds1.t_order_1。配置是这样的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_inline key-generate-strategy: column: order_id key-generator-name: order_snowflake sharding-algorithms: order_inline: type: INLINE props: algorithm-expression: t_order_$-{order_id % 2} key-generators: order_snowflake: type: SNOWFLAKE这里的actual-data-nodes用了一个Groovy表达式ds$-{0..1}.t_order_$-{0..1}读作“ds0和ds1下的t_order_0和t_order_1”。注意这个表达式里的$-{}是ShardingSphere的内联表达式语法不能省略花括号。配置分片算法时INLINE类型支持简单取模也支持自定义表达式。这里我用的是order_id % 2也就是订单ID对2取模结果为0进t_order_0结果为1进t_order_1。3.3 分布式ID与分片键的绑定关系分片键选哪个字段直接决定了数据能不能均匀散列。很多人习惯用自增主键当分片键但自增主键在分布式场景下很容易出现跨库重复、写入热点等脏问题。ShardingSphere-JDBC内置了雪花算法SNOWFLAKE配合key-generate-strategy自动生成全局唯一ID这个ID再作为分片键参与取模效果很好。配置里key-generate-strategy指定了生成策略作用的列是order_id生成器名称指向order_snowflake。然后key-generators下面定义order_snowflake的类型为SNOWFLAKE。在实际插入数据的时候如果实体里order_id没有赋值ShardingSphere会自动帮我们生成一个雪花ID填充到这个字段里不需要我们手动处理。需要注意的一个坑是SNOWFLAKE生成的ID是一个Long类型如果你的实体里把主键定义成了Integer插入时就会出现类型转换异常。我在项目里遇到过一个线上问题就是代码里把订单ID定义成了Integer业务量小的时候看不出问题数据量一上来雪花ID超出Integer最大值直接把应用打挂了。所以使用ShardingSphere-JDBC的分布式ID实体主键务必用Long。3.4 读写分离配置5.x改了什么如果单库的压力不只是数据量问题还有读写比例失衡问题可以在分片规则之外再加一套读写分离规则。5.x之后原来老版本里那个masterslave配置已经被废弃了现在要用readwrite-splitting节点来配置。下面是一个简单的示例把ds0作为写库ds1和ds2作为读库读请求按轮询负载均衡spring: shardingsphere: rules: readwrite-splitting: >CREATE TABLE t_order_0 ( order_id bigint NOT NULL, user_id bigint NOT NULL, order_amount decimal(10,2) NOT NULL, order_status varchar(32) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;order_ds0把t_order_0和t_order_1都建好order_ds1里同样建好这两张表。注意ShardingSphere-JDBC只负责路由SQL不负责维护物理表结构的一致性。如果后面表结构变更哪怕改了逻辑表的字段也必须在多个物理表上同步执行DDL否则业务查询会报字段不存在。4.2 实体与Mapper的写法实体类的定义要配合分片键类型。按照前面的配置order_id使用雪花算法生成所以实体里用Long定义主键。另外插入时不要给order_id赋值否则ShardingSphere会认为你想用自己指定的主键值而不再走雪花生成策略。Data TableName(t_order) public class Order { TableId(type IdType.INPUT) private Long orderId; private Long userId; private BigDecimal orderAmount; private String orderStatus; private LocalDateTime createTime; }这里TableId(type IdType.INPUT)是MyBatis-Plus的写法表示主键由外部输入因为真正的ID生成是ShardingSphere在SQL执行前处理的MyBatis-Plus自身不需要生成。如果这里配成IdType.AUTOMyBatis-Plus会以为要用数据库自增在解析SQL时反而可能出问题。Mapper接口就是普通的MyBatis-Plus BaseMapperMapper public interface OrderMapper extends BaseMapperOrder { }4.3 插入、查询、分页场景实测插入数据时只需要调用insert(order)方法ShardingSphere-JDBC会自动解析SQL中的逻辑表t_order根据order_id的空值生成雪花ID然后根据生成的ID进行取模路由。我们可以通过开启SQL日志来观察路由结果。在spring.shardingsphere.props下面配置sql-show: true启动后控制台会打印每一条逻辑SQL对应的真实SQL和路由节点格式大致如下Actual SQL: ds0 ::: INSERT INTO t_order_0 (order_id, user_id, ...) VALUES (?, ?, ...)当我们看到这条日志说明数据已经被正确路由到ds0.t_order_0物理表了。注意sql-show在生产环境建议关闭因为它会打印所有SQL既影响性能又有信息泄露风险我一般只在测试环境打开。查询场景下如果查询条件带上了分片键order_idShardingSphere-JDBC可以精准定位到某一张物理表性能最好。比如执行Order order orderMapper.selectById(123456789L);它会根据order_id取模直接路由到对应的物理表。但如果查询条件没有分片键比如只查order_statusShardingSphere-JDBC就会广播到所有的物理表最后做结果合并。这种全节点扫描在数据量大时是灾难所以业务设计上要尽量避免这种查询。分页查询方面MyBatis-Plus的Page对象配合ShardingSphere-JDBC能正常使用但要注意它实现分页的方式其实是把LIMIT下推到每个物理库再在内存中合并排序。这意味着假如你查第100万条它会把每个物理表前100万条都查出来再做内存合并深分页场景内存压力极大。我在项目里一般限制查询深度不让用户往很深页码翻这是最有效的优化手段。4.4 绑定表与广播表的处理分库分表之后多表关联查询会变得非常麻烦。比如订单主表和订单明细表如果订单按order_id分片而订单明细表也按order_id分片理论上t_order和t_order_item可以在同一个物理库的同一张物理表上执行关联查询。ShardingSphere-JDBC通过绑定表配置把这个信息告诉路由模块tables: t_order: # 省略其他配置 t_order_item: actual-data-nodes: ds$-{0..1}.t_order_item_$-{0..1} table-strategy: standard: sharding-column: order_id sharding-algorithm-name: order_inline binding-tables: - t_order,t_order_item这里绑定表要求分片键一致、分片算法一致否则关联查询时两个表可能路由到不同的物理节点最终结果就是查不到数据或者数据不完整。至于广播表主要用在配置类的数据上比如地区表、字典表所有分片表都要带着这类数据去关联所以干脆在每个物理库都保存一份全量数据broadcast-tables: - t_config广播表的写入会广播到所有物理表查询则从任意一个节点读取这种设计是典型的空间换路由效率的做法。我在实际项目里只要遇到字典表、全局配置表都无脑设成广播表因为这类表基本没有大字段数据量也不大。5. 常见问题与排查技巧5.1 启动报错数据源无法注入或BeanDefinitionOverrideException这是接入时最典型的报错。如果你的配置里没有加spring.main.allow-bean-definition-overriding: true启动时大概率会报BeanDefinitionOverride相关的异常。解决办法就是加上这个配置项同时注意检查spring.shardingsphere.datasource.names里配置的数据源名称和后面的分片规则里引用的名称是否完全一致。名称对不上启动时会提示找不到对应数据源。另外一个奇怪但很常见的问题是有些同学在application.yml里同时配置了Spring Boot原生的spring.datasource和ShardingSphere的spring.shardingsphere.datasource。这样会导致原生数据源和ShardingSphere数据源同时存在MyBatis可能会注入到原生数据源上导致分片不生效。我建议一旦使用ShardingSphere-JDBC就不要再配置spring.datasource把数据源全部交给ShardingSphere管理。5.2 分页与聚合查询结果异常分页查询如果出现数据重复或缺失第一排查方向是分片算法的正确性。比如你配置了多个分片键但查询时只传了一个分片键路由就可能无法精确匹配最终结果合并出错。另一个现象是排序不一致ShardingSphere-JDBC会将排序字段下推到每个物理表再在内存中做总排序但如果排序字段没有包含在结果集的合并字段里可能会出现乱序。解决办法是尽量在查询SQL中显式指定ORDER BY让ShardingSphere明确知道排序字段。聚合查询比如SUM、COUNTShardingSphere-JDBC会把聚合操作分发到每个物理表最后在内存中合并。对于COUNT(*)这种简单聚合结果一般是正确的。但对于AVG这种聚合ShardingSphere-JDBC会先分别聚合出总和和数量再做除法这样可以避免简单求平均带来的误差。不过如果SQL里同时存在多个聚合和GROUP BY路由合并的复杂度会成倍增加我建议在业务层面尽量减少这种查询必要时走单独的报表库而不是在分片库上做重度聚合。5.3 事务问题LOCAL、XA还是SEATAShardingSphere-JDBC默认的事务类型是LOCAL也就是本地事务。在LOCAL事务下如果一次操作只涉及同一个物理库里的数据事务是生效的。但一旦跨库更新比如同时往ds0和ds1各写一条数据LOCAL事务就无法保证原子性了因为两个库之间的提交和回滚没有协调机制。如果你有跨库事务的需求官方提供了XA事务和SEATA分布式事务两种方案。XA需要引入额外的依赖配置相对简单SEATA则更适合长事务和复杂的业务回滚场景。我在实际项目中用的是SEATA因为我们的业务不只是跨库还有跨服务调用需要全局事务协调。需要强调的是引入分布式事务之后原来基于Spring的Transactional注解仍然是入口但底层的执行逻辑交给了分布式事务管理器如果某个参与事务的微服务没有正确接入事务协调器就会出现全局事务失效但应用不报错的诡异情况。排查这种问题关键是看日志里是否出现事务分支的注册记录。5.4 配置文件里的坑与速查表我把几个最容易踩的坑整理成一张表方便你对照检查。问题现象常见原因解决方式启动找不到数据源datasource.names与规则中引用名称不一致统一逻辑名称插入数据ID超限主键类型用了Integer改成Long分片不生效配置了原生spring.datasource删掉原生数据源配置SQL执行路由到全部节点查询条件没有分片键尽量按分片键查询跨库操作不回滚事务类型是LOCAL引入XA或SEATA深分页内存溢出大偏移量limit查询限制深度使用游标或子查询另外说一个非常容易被忽略的配置spring.shardingsphere.props.sql-show在测试环境开在预发和生产环境一定要关否则日志量会大得吓人。还有spring.shardingsphere.props.sql-simple这个参数它会打印简单的SQL信息而不打印参数绑定列表排查问题时信息量不够我一般还是用默认的sql-show。5.5 版本升级时的配置兼容如果你是从4.x升级到5.5.0只会改一个依赖版本是不够的。4.x时代那种sharding.jdbc.config.datasource的写法必须全面改成spring.shardingsphere.datasource分片规则里sharding-column的配置方式也变成了嵌套在table-strategy.standard下面。算法配置从原来的precise-algorithm-class变成了sharding-algorithms节点里声明指定的算法类型和表达式。直接拿4.x的配置往5.5.0里贴启动时会报属性不识别不要慌按着5.x的规范慢慢改就好。我在一次旧系统升级时还遇到过一个问题4.x版本里自定义的分片算法类在升级到5.5.0之后无法直接使用。原因很简单5.x的分片算法SPI接口签名变了原来实现的那种接口已经废弃。这种情况下需要重写自定义算法类并按照新SPI规范在META-INF/services目录下声明实现类。这块如果不熟建议先把自定义算法改成官方自带的INLINE或HASH_MOD算法等稳定之后再考虑恢复自定义逻辑。我在实际操作中还有一个小习惯就是把分片键的选择当成表设计的一部分来提前规划而不是等代码写完了再回来调整。因为分片键一旦定了后续想改不仅仅是改配置那么简单存量数据迁移才是最头疼的。所以如果你现在还在设计阶段千万要挑业务上最常见、最稳定的查询维度作为分片键比如订单号、用户ID这些天然均匀分布的字段别选状态、类型这种枚举值很少的字段。这套基础配置跑通之后后续还可以往数据加密、影子库压测这些方向去扩展。ShardingSphere-JDBC 5.5.0的强大之处就在于它的规则引擎是统一抽象的分片和加密可以叠加在同一条SQL上互不干扰。只要把基础链路摸熟后面的高阶玩法上手会快很多。
返回列表