news 2026/10/22 4:15:57

ShardingSphere与@DS注解共存方案:数据源切换与分片路由的职责分离

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ShardingSphere与@DS注解共存方案:数据源切换与分片路由的职责分离

最近在做一个订单类的演示项目,技术选型时同时用到了 ShardingSphere 和@DS注解。本来觉得这两者各管各的没什么问题,结果一跑起来就出事:要么 ShardingSphere 的分片规则完全不生效,要么@DS切数据源像没写一样,日志里 SQL 全部打到默认库。后来顺着数据源链路排查,才发现问题不在业务代码,而在两个框架对 DataSource 的争夺和职责边界上。这篇文章就把这套整合方案掰开揉碎讲清楚,适合正在用 ShardingSphere 做分库分表、同时又依赖@DS做多数据源切换的团队参考。

先说结论:ShardingSphere 和@DS完全可以共存,但前提是别把两者放在同一层“抢 DataSource 控制权”。正确做法是让@DS负责“切哪个逻辑数据源”,让 ShardingSphere 负责“在逻辑数据源内部做分片路由”。下面先从底层机制说起。

1. 两条链路各管什么:@DS 的切源和 ShardingSphere 的路由为什么不能互相替代

很多第一次遇到这个组合的人,脑子里会有一个直觉:ShardingSphere 已经管理了多个物理库,@DS又能在多个数据源之间切换,那我是不是可以在@DS("ds_0")和@DS("ds_1")之间切换,顺便实现分库?这个想法看着合理,实际上会把 ShardingSphere 完全绕过去,造成分片逻辑失效、SQL 路由错乱、甚至表都找不到。

1.1 @DS 的切源原理

@DS注解来自 dynamic-datasource 框架,核心机制并不复杂:通过 AOP 拦截被注解修饰的方法,把注解里指定的数据源 key 放到一个 ThreadLocal 里。然后业务代码通过注入的 DataSource 获取连接时,动态路由数据源会去一个Map<String, DataSource>里根据 key 找到对应的真实数据源,再返回它的 Connection。

整个过程就是“切 DataSource”,它不关心 SQL 是 INSERT 还是 SELECT,也不关心表名落在哪个库。只要 key 存在,它就把连接切换到那个数据源;key 不存在,默认落到 primary 数据源。

1.2 ShardingSphere 的路由原理

ShardingSphere-JDBC 的本质是一个代理数据源,它也实现了javax.sql.DataSource接口,但它内部不直接持有单一物理连接,而是持有多个真实数据源(比如 ds_0、ds_1),同时维护一套分片规则。当业务 SQL 穿过它的时候,它会解析 SQL、根据分片键计算路由结果,再把 SQL 改写成目标库目标表上的 SQL,最后执行并归并结果。

也就是说,ShardingSphere 的工作范围是“在数据源内部做路由”,它控制的是 SQL 级别的物理路由。它和@DS并不在一个维度上。

1.3 冲突本质:两个框架都在抢 DataSource

真正让两者打架的,不是注解本身,而是 Spring 容器里的 DataSource Bean 归属问题。

用 dynamic-datasource 时,它会把一个动态路由数据源作为 primary DataSource 注册进容器。用 ShardingSphere 的 Spring Boot Starter 时,它也会尝试注册一个 ShardingSphereDataSource 作为数据源。如果两个 starter 同时引入,又没有做排除,Spring 容器里就会出现两个“疑似” DataSource 的 Bean,注入时谁被选中就成了看运气的事。

即便 Bean 不冲突,另一个常见问题是职责错位。有的项目把@DS("ds_0")写在 Service 方法上,以为这样就能把 ShardingSphere 绕开手动选库,这等于放弃了分片规则。ShardingSphere 一旦发现 SQL 路由后的表不存在,就会报出各种奇怪的表解析错误,而且这类问题最难排查。

给这两者做个简单对比:

维度@DS 注解ShardingSphere
控制粒度方法级数据源切换SQL 级分片路由
管理对象多个逻辑 DataSource多个物理 DataSource
切源时机AOP 拦截,ThreadLocal 传 keySQL 解析时计算路由
常见误区用来指定分片库用来做业务级数据源切换

所以整合的关键,不是让其中一个适应另一个,而是把它们的职责分开。ShardingSphere 应该作为一个整体数据源被@DS管理,而不是让@DS去管理 ShardingSphere 里的每一个分片库。

2. 整合架构选型:把 ShardingSphere 包成普通数据源塞给动态路由

明确了职责边界后,后面的问题就简单了:我们到底让谁当 Spring 容器里的 “DataSource 总入口”?

2.1 方案 A:ShardingSphere 当总入口,@DS 只做静态选库

这个方案思路是:应用只暴露一个 ShardingSphereDataSource,把需要分库分表的表全部纳入 ShardingSphere 的规则配置中,@DS只用来切换“非 ShardingSphere 管理的普通库”。

听起来很干净,但实际会碰到一个尴尬情况:ShardingSphere 的规则是全局配置的,它管理的表会按照规则路由。假如业务里有 t_order 这类分片表,也有不需要分片的 t_user、t_log,你就得在 ShardingSphere 里给每张表都配置规则。不需要分片的表也要配成 broadcast table 或者在规则里做处理,否则 SQL 路由会把它当成单表,直接往一个默认数据源上执行,行为不可控。

我见过不少项目为了把不相关表塞进 ShardingSphere 规则里,配置越写越厚,维护成本直线上升。这个方案适合“几乎全部表都需要分片”的项目,不适合有多套独立业务库的混合场景。

2.2 方案 B:dynamic-datasource 当总入口,ShardingSphere 作为其中一个逻辑数据源

这是我最推荐的做法,也是后面配置示例里采用的方式。

具体思路:dynamic-datasource 继续担任动态路由数据源,负责管理所有逻辑数据源。那些不需要分片的库,直接用普通连接池配置即可。而需要分库分表的库,用代码手动构建一个 ShardingSphereDataSource,然后通过 dynamic-datasource 的注册接口把它挂到某个 key 下面,比如sharding-order。

业务调用时:

  • 想走分片数据源,就写@DS("sharding-order");
  • 想走普通日志库,就写@DS("log-db");
  • 默认不写注解,落到 primary 数据源。

这个方案的好处是层次清晰:@DS只做“逻辑数据源选择”,ShardingSphere 只做“物理分片路由”。两者各自在自己的层次里工作,互不干扰。ShardingSphere 对上层应用来说,就是一个普通的 DataSource,它可以被放进任何支持 DataSource 管理的框架里。

2.3 方案 C:ShardingSphere 只做分表,不做分库,跨库交给 @DS

有些项目的数据量还没到需要分库的程度,只是想解决单表数据量过大的问题。这种情况下可以让 ShardingSphere 只做单库分表,多个业务库之间用@DS切换。

这种组合也很实用,尤其适合“订单库单独分表 + 其他库普通管理”的场景。需要注意 ShardingSphere 的规则配置里只需要配置表规则,数据源名只有一个,不需要配置分库策略。

2.4 方案取舍

方案配置复杂度灵活性适用场景
A:ShardingSphere 总入口高,所有表都要纳入规则低,普通表处理麻烦全库分片项目
B:dynamic 总入口,sharding 作为一个数据源中,手动构建 sharding 数据源一次高,混合业务库友好分片库和普通库共存的项目
C:sharding 只分表,@DS 负责跨库低,无分库策略中单库分表为主,跨库较少

我在实际项目里选择的是方案 B。理由很简单:项目里既有需要分片的订单表,又有大量不需要分片的配置表和日志表,单独一套 ShardingSphere 规则不可能同时满足两类表的路由需求,而动态数据源的 key 管理方式可以让业务代码非常直观。

3. 最小可运行整合配置:依赖排除、数据源构建与注册

选型定了,下面直接上可运行的配置。这里以 Spring Boot 2.7 + dynamic-datasource 4.x + ShardingSphere-JDBC 5.x 为例,注意 ShardingSphere 各版本之间的 API 差异比较大,尤其是分片算法配置这一块,下面代码中的写法在 5.4 版本下是可以用的。

3.1 依赖引入:避免两个 Starter 自动配置互抢

ShardingSphere 官方提供了 Spring Boot Starter,引入后会自动配置一个 ShardingSphereDataSource,但在 dynamic-datasource 同时存在时,容易发生 DataSource Bean 冲突。所以我建议只引入 ShardingSphere 的 core 依赖,不使用它的 Spring Boot Starter,用它提供的 API 手动构建数据源。

<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>4.3.1</version> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core</artifactId> <version>5.4.1</version> </dependency>

如果你之前已经引入了shardingsphere-jdbc-spring-boot-starter,建议先移除,或者把它的自动配置排除掉。我自己的经验是:与其在排除配置里来回折腾,不如直接用 core 手动构建,至少数据源是怎么创建出来的,心里是清楚的。

3.2 手动构建 ShardingSphere 数据源

下面是创建一个分库分表数据源的配置类。这里模拟两个物理库 ds_0、ds_1,每个库里各有一张订单分表 t_order_0、t_order_1。分库键用 user_id 取模,分表键用 order_id 取模。

@Configuration public class ShardingDataSourceConfig { @Bean("shardingOrderDataSource") public DataSource shardingOrderDataSource() throws SQLException { Map<String, DataSource> dataSourceMap = new LinkedHashMap<>(); dataSourceMap.put("ds_0", createDataSource("jdbc:mysql://127.0.0.1:3306/ds_0?useSSL=false&serverTimezone=Asia/Shanghai")); dataSourceMap.put("ds_1", createDataSource("jdbc:mysql://127.0.0.1:3306/ds_1?useSSL=false&serverTimezone=Asia/Shanghai")); ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration(); ShardingTableRuleConfiguration orderTableRule = new ShardingTableRuleConfiguration( "t_order", "ds_${0..1}.t_order_${0..1}" ); shardingRuleConfig.getTables().add(orderTableRule); shardingRuleConfig.setDefaultDatabaseStrategy( new InlineShardingStrategyConfiguration("user_id", "ds_${user_id % 2}") ); shardingRuleConfig.setDefaultTableStrategy( new InlineShardingStrategyConfiguration("order_id", "t_order_${order_id % 2}") ); Properties props = new Properties(); props.setProperty("sql-show", "true"); return ShardingSphereDataSourceFactory.createDataSource( dataSourceMap, Collections.singleton(shardingRuleConfig), props ); } private DataSource createDataSource(String jdbcUrl) { HikariConfig config = new HikariConfig(); config.setJdbcUrl(jdbcUrl); config.setUsername("root"); config.setPassword("123456"); config.setDriverClassName("com.mysql.cj.jdbc.Driver"); return new HikariDataSource(config); } }

这里有几个细节值得注意。

数据源 map 里的 keyds_0、ds_1是给 ShardingSphere 路由用的名字,和业务代码里的@DSkey 没有关系。分片表达式里的${0..1}是范围枚举,${user_id % 2}是 inline 取模,这两条表达式可以直接帮助 ShardingSphere 算出目标库和目标表。

sql-show打开后,ShardingSphere 会在控制台打印逻辑 SQL 和实际 SQL,这是后面验证路由结果最重要的手段,开发阶段建议打开,性能敏感时再关闭。

3.3 把 ShardingSphere 数据源注册进动态路由

手动构建的 ShardingSphere 数据源并不会自动被 dynamic-datasource 识别,必须把它注册进动态路由数据源的 key 仓库里。我的做法是写一个启动后的注册器,而不是在配置类构建阶段直接注入 dynamic 数据源,因为这时候 Bean 可能还没有初始化完,容易踩循环依赖。

@Component public class ShardingDataSourceRegistrar implements ApplicationRunner { private final DynamicRoutingDataSource dynamicRoutingDataSource; private final DataSource shardingOrderDataSource; public ShardingDataSourceRegistrar( DynamicRoutingDataSource dynamicRoutingDataSource, @Qualifier("shardingOrderDataSource") DataSource shardingOrderDataSource) { this.dynamicRoutingDataSource = dynamicRoutingDataSource; this.shardingOrderDataSource = shardingOrderDataSource; } @Override public void run(ApplicationArguments args) { String dsName = "sharding-order"; if (dynamicRoutingDataSource.getDataSource(dsName) == null) { dynamicRoutingDataSource.addDataSource(dsName, shardingOrderDataSource); } } }

这段代码把 keysharding-order绑定到 ShardingSphere 数据源上。之后业务代码里的@DS("sharding-order")就会拿到这个整体数据源,SQL 进入后会由 ShardingSphere 完成分片路由。

注意这里的DynamicRoutingDataSource是 dynamic-datasource 内部实现类。在 dynamic-datasource 4.x 版本中,默认注册的 DataSource Bean 就是它,构造器里直接注入即可。如果当前容器里有多个 DataSource Bean,记得给注入的数据源加上@Qualifier,避免注入到别的数据源。

3.4 application.yml:普通库与分片库的共存配置

普通数据源仍然走 dynamic-datasource 的 yml 配置。这里我配置了一个日志库 log-db 作为 primary,这样所有没写@DS的方法都会默认落到 log-db。

spring: datasource: dynamic: primary: log-db strict: false datasource: log-db: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/log_db?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456

strict 设置为 false 是为了保证 key 不存在时能优雅回退到 primary,而不是直接抛异常。开发阶段可以先开着 strict 调试,避免悄悄回退掩盖问题。

3.5 Service 层使用方式

注册完成后,使用起来就很直接了。这里是两个典型的示例方法:

@Service public class OrderService { @DS("sharding-order") public void addOrder(OrderDO order) { orderMapper.insert(order); } @DS("sharding-order") public List<OrderDO> queryByUserId(Long userId) { return orderMapper.selectByUserId(userId); } }

日志操作走普通库:

@Service public class LogService { @DS("log-db") public void addLog(LogDO log) { logMapper.insert(log); } }

当执行addOrder时,@DS拿到的是 ShardingSphere 数据源,SQL 进入 ShardingSphere 后被解析、改写,最终落到 ds_0 或 ds_1 的 t_order_0 或 t_order_1。当执行addLog时,直接走 log-db,完全绕开分片逻辑。

这就是整个整合方案最核心的使用方式:业务层通过@DS选择逻辑数据源,分片 SQL 的内部路由全部交给 ShardingSphere。

4. 事务、代理与初始化顺序:三个高频翻车点及处理方式

配置跑通之后,紧接着会遇到三个比较隐蔽的问题,分别出在事务、代理对象和 Bean 初始化顺序上。这些问题在功能简单时不会暴露,一旦加上事务或者并发访问,就容易出现“偶发但致命”的故障。

4.1 事务边界与 @DS 切换顺序

这是一个非常经典的坑。很多人这样写:

@Transactional public void doBiz() { orderService.addOrder(order); logService.addLog(log); }

@Transactional注解的事务管理器在方法执行开始时,就会从 DataSource 获取连接并把它绑定到当前线程。等执行到orderService.addOrder时,@DS再切数据源已经晚了,因为事务管理器手里的连接还是最开始那个。

如果这个事务入口本身用的是 primary 数据源,那么即使方法内部写了@DS("sharding-order"),SQL 也还是会落到 primary 对应的物理连接上。这种问题日志看不出来,因为代码确实执行了,只是数据跑到了错误的库里,非常难排查。

处理方式有三种:

  1. 拆分事务边界,把@Transactional放在真正需要事务的单个数据源操作上,让@DS切换发生在事务边界之外;
  2. 使用 dynamic-datasource 提供的@DSTransactional,它能感知路由数据源的切换,在同一个事务里支持多数据源连接绑定;
  3. 如果必须使用原生@Transactional,就保证事务方法内部只访问同一个逻辑数据源。

我在项目里更倾向于第一种。把“跨库操作不能共享本地事务”这件事想在前面,远比后面去排查诡异数据要省事。ShardingSphere 的 Connection 本身是逻辑连接,真正的物理连接在 SQL 执行时才确定,跨分片的本地事务并不能像单库事务那样保证强一致。如果业务对一致性要求高,就需要考虑分布式事务方案了。

4.2 数据源 Bean 初始化顺序与循环依赖

如果尝试在配置类方法里直接把 ShardingSphereDataSource 注入 DynamicRoutingDataSource,可能会遇到BeanCurrentlyInCreationException这类循环依赖问题。因为 dynamic-datasource 的自动配置和手动创建的 sharding 数据源之间存在初始化先后关系,不好靠不容易控制的@Order去解决。

我建议不要在 Bean 创建阶段做注册,而是放到ApplicationRunner里,也就是前面示例代码的写法。这个时机点里所有 Bean 都已经创建完成,Spring 容器已经处于可用状态,调用 addDataSource 是安全的。这样既避开了循环依赖,也避免了数据源 key 在业务还没开始时就不可用的问题。

4.3 事务管理器到底该绑定哪一个数据源

Spring Boot 自动配置的事务管理器默认绑定的是容器里的 primary DataSource。在我们的整合方案里,dynamic-datasource 的动态路由数据源应该是 primary,所以事务管理器绑定的也就是这个动态路由数据源。事务开始时从路由数据源拿连接,路由数据源再根据当前线程里的 key 去决定返回哪个真实连接。

这也是为什么事务管理器只能有一套。如果你又额外给 ShardingSphere 数据源单独创建了一个事务管理器,业务代码里又混着使用,很容易出现事务不回滚、连接被提前释放的问题。

实测下来,保持“一个动态路由数据源 + 一个事务管理器”是最简单的。内部多数据源的事务一致性需求,用 dynamic-datasource 提供的@DSTransactional兜底,不要自己造多事务管理器。

5. 实测验证与经验清单:路由日志、异常表现和最终建议

配置完成后,我用一个模拟订单系统做了验证,核心目标是确认两点:分片路由是否真的生效,以及普通库和分片库之间是否能通过@DS正常切换。

5.1 测试环境与用例

搭建了两套物理库:ds_0 和 ds_1,每个库里创建了 t_order_0、t_order_1 两张分表;另一个独立库 log_db 存放日志。表结构保持一致,分表字段为 order_id,分库字段为 user_id。

测试用例包括:

  1. 插入订单,观察路由日志能否正确计算到 ds_0 / ds_1 以及对应的 t_order_0 / t_order_1;
  2. 查询时使用 userId 作为条件,观察是否只命中一个分片;
  3. 插入日志到 log_db,确认没有经过 ShardingSphere 路由;
  4. 在一个方法里同时操作两个数据源,验证@DS切换是否生效。

5.2 路由日志验证

打开sql-show后,执行插入订单时控制台会输出类似下面的内容:

Logic SQL: INSERT INTO t_order (order_id, user_id, amount) VALUES (?, ?, ?) Actual SQL: ds_0 ::: INSERT INTO t_order_0 (order_id, user_id, amount) VALUES (?, ?, ?)

这条日志非常关键。看到 Actual SQL 里的ds_0 ::: t_order_0,就说明 ShardingSphere 已经按照 user_id 取模计算出了目标库,按照 order_id 取模计算出了目标表。如果这里输出的是ds_0 ::: t_order,说明表规则没有生效,大概率是分表表达式写错了。

执行日志插入时,控制台不会出现 ShardingSphere 的 Logic SQL 输出,说明这条 SQL 没有通过 ShardingSphere 数据源,@DS("log-db")的切换是生效的。

5.3 常见异常与原因速查

异常或现象可能原因处理方式
DataSource Bean 冲突,注入报错ShardingSphere Starter 和 dynamic-datasource 同时注册数据源改用 shardingsphere-jdbc-core,手动构建 sharding 数据源
@DS 开了,但 SQL 仍走 primary事务先开启,连接已绑定调整事务边界,或用 @DSTransactional
出现”表不存在”但表明明建了分表表达式把表路由到了不存在的物理表检查 inline 表达式和实际建表名是否一致
ShardingSphere 规则完全没生效业务方法没有走 sharding 数据源确认 @DS key 对应的是 ShardingSphereDataSource
动态路由 key 找不到注册时机太早,addDataSource 没执行移到 ApplicationRunner 阶段注册

5.4 最终建议

整套方案跑通后,我对这类整合场景的体会是:先画数据源关系图,再写代码。把“逻辑数据源”和“物理数据源”分开画,标注哪些库由 ShardingSphere 管理,哪些库由动态路由直接切,哪些表需要分片,哪些表不能碰分片规则。这张图画清楚了,后面所有配置和注解都是水到渠成的事。

最后再分享一个小技巧:开发阶段把 dynamic-datasource 的 strict 设为 true,把 ShardingSphere 的 sql-show 打开,一旦 SQL 路由到意料之外的库,日志能第一时间暴露问题。上线前再关掉这两个开关,性能和安全都更稳妥。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 4:15:37

Windows相对路径快捷方式制作:BAT脚本实现便携应用路径自适应

1. 为什么需要相对路径快捷方式1.1 从一次真实的翻车经历说起去年帮一个做影视后期的朋友处理过一台工作机&#xff0c;他习惯把项目素材放在移动硬盘里&#xff0c;桌面放了一堆快捷方式。结果硬盘盘符从E:变成F:之后&#xff0c;所有快捷方式全部失效&#xff0c;双击弹窗提示…

作者头像 李华
网站建设 2026/10/22 4:14:59

图像分类完整实战流程:从数据预处理到模型部署的关键技术解析

图像分类这堂课&#xff0c;终于轮到动手了。前几节我们还在和数学公式、网络结构图打交道&#xff0c;到了第五节&#xff0c;很多人的第一个直观感受是&#xff1a;原来从零把一个分类模型训练到能看的程度&#xff0c;中间要踩的坑比想象中多得多。这一节的内容比较贴近实际…

作者头像 李华
网站建设 2026/10/22 4:14:53

机器学习驱动5G信道估计:任务拆分、数据与工程实践

简介&#xff1a;面向5G信道估计与机器学习交叉领域的研究者与开发者&#xff0c;这份资源聚焦Massive MIMO与OFDM场景下利用深度学习优化信道状态信息&#xff08;CSI&#xff09;估计的完整实现&#xff0c;解决传统信道估计难以应对高频率和复杂多址技术的问题。压缩包共14个…

作者头像 李华
网站建设 2026/10/22 4:14:39

深入解析Linux内核kmalloc_cache结构与内存泄漏排查实战

1. 从一次内存泄漏排查说起前阵子帮一个朋友看他们服务进程的内存增长问题&#xff0c;/proc/meminfo里Slab那一栏涨得离谱&#xff0c;但业务侧的对象数量明明没有明显增加。用slabtop一拉&#xff0c;发现kmalloc-256这个缓存条目数量异常高&#xff0c;而且只增不减。当时第…

作者头像 李华
网站建设 2026/10/22 4:14:38

用提示工程重构培训内容,志愿者参与度提升85%

我先把这个项目放在一个具体的场景里说清楚&#xff1a;一家做社区志愿服务、运营了五六年的公益组织&#xff0c;过去两年最头疼的问题不是缺人&#xff0c;而是留不住人。志愿者报名很积极&#xff0c;但培训完一轮之后&#xff0c;真正愿意持续参加服务的人连一半都不到。很…

作者头像 李华
网站建设 2026/10/22 4:14:38

AI信息过载时代:三层过滤法与最小可验证实验

1. 当"AI参考"成为一个栏目&#xff1a;我在信息过载里做减法的真实动机"AI参考第1期"这个标题摆在我面前的时候&#xff0c;我第一反应不是"要写点什么"&#xff0c;而是"我到底想给谁看"。过去大半年&#xff0c;我陆陆续续被身边做…

作者头像 李华