news 2026/7/21 7:34:44

多数据源切换:@DS 注解底层调用原理深度剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多数据源切换:@DS 注解底层调用原理深度剖析

多数据源切换:@DS 注解底层调用原理深度剖析

一、概述

@DS注解来自dynamic-datasource-spring-boot-starter组件(苞米豆出品),并非 MyBatis-Plus 核心包,而是其生态扩展。该注解用于在多数据源场景下,声明式地指定当前方法/类使用哪个数据源。

使用示例

@DS("xxx")// 切换到指定数据源@Transactional(propagation=Propagation.REQUIRES_NEW)publicclassOrderDaoImplextendsServiceImpl<OrderMapper,OrderEntity>{// ...}

二、核心组件与调用链

2.1 整体架构

@DS 注解 │ ▼ DynamicDataSourceAnnotationInterceptor (AOP 拦截器) │ ▼ DynamicDataSourceClassResolver (解析注解,确定数据源名称) │ ▼ DynamicDataSourceContextHolder (ThreadLocal 存储当前数据源标识) │ ▼ DynamicRoutingDataSource (继承 AbstractRoutingDataSource,路由数据源) │ ▼ DataSourceProperty → HikariCP/Druid (实际物理数据源连接池)

2.2 六大核心组件

组件职责
@DS注解,标记目标数据源名称
DynamicDataSourceAnnotationInterceptorAOP MethodInterceptor,拦截被@DS标注的方法
DynamicDataSourceClassResolver解析@DS注解,处理方法级/类级优先级
DynamicDataSourceContextHolder基于 ThreadLocal 持有当前线程的数据源标识
DynamicRoutingDataSource继承AbstractRoutingDataSource,根据标识路由到真实数据源
DataSourceProperty封装每个数据源的配置(url、username、pool 等)

三、底层调用原理详解

3.1 自动配置阶段

dynamic-datasource-spring-boot-starter通过 Spring Boot 自动配置机制注册核心 Bean:

spring.factories / AutoConfiguration │ ├── DynamicDataSourceAutoConfiguration │ ├── 注册 DynamicDataSourceProperties(读取 spring.datasource.dynamic.* 配置) │ ├── 注册 DynamicRoutingDataSource(主数据源 Bean,替换默认 DataSource) │ ├── 注册 DynamicDataSourceAnnotationAdvisor(AOP 切面) │ └── 注册各个数据源连接池(master、slave、自定义等) │ └── DynamicDataSourceAopConfiguration └── 注册 DynamicDataSourceAnnotationInterceptor + 切点

关键点DynamicRoutingDataSource替换了 Spring 默认的DataSourceBean,所有数据库操作都经过它路由。

3.2 AOP 拦截阶段

当调用被@DS标注的方法时,执行流程如下:

1. 调用方调用 @DS 标注的方法 │ 2. DynamicDataSourceAnnotationInterceptor.invoke() │ 3. DynamicDataSourceClassResolver.findKey(method, targetClass) │ ┌─────────────────────────────────────────────┐ │ │ 注解查找优先级(从高到低): │ │ │ ① 方法上的 @DS │ │ │ ② 类上的 @DS │ │ │ ③ 接口方法上的 @DS │ │ │ ④ 接口类上的 @DS │ │ │ ⑤ 默认数据源(master) │ │ └─────────────────────────────────────────────┘ │ 4. DynamicDataSourceContextHolder.push(key) │ 将数据源标识压入 ThreadLocal 栈(支持嵌套切换) │ 5. 执行实际业务方法 → MyBatis 执行 SQL │ 6. DynamicRoutingDataSource.determineTargetDataSource() │ 从 ThreadLocal 取出当前数据源标识 │ 根据标识从 Map<String, DataSource> 中获取对应数据源 │ 7. 方法执行完毕 │ 8. DynamicDataSourceContextHolder.poll() │ 弹出栈顶数据源标识,恢复上一层数据源

3.3 核心源码逻辑

(1)AOP 拦截器
publicclassDynamicDataSourceAnnotationInterceptorimplementsMethodInterceptor{@OverridepublicObjectinvoke(MethodInvocationinvocation)throwsThrowable{// 1. 解析当前方法/类上的 @DS 注解,获取数据源名称StringdsKey=dynamicDataSourceClassResolver.findKey(invocation.getMethod(),invocation.getThis().getClass());// 2. 将数据源标识压入 ThreadLocalDynamicDataSourceContextHolder.push(dsKey);try{// 3. 执行实际方法(此时 SQL 执行会走路由数据源)returninvocation.proceed();}finally{// 4. 方法结束后弹出数据源标识,恢复上层DynamicDataSourceContextHolder.poll();}}}
(2)ThreadLocal 上下文持有者
publicclassDynamicDataSourceContextHolder{// 使用栈结构支持嵌套 @DS 调用privatestaticfinalThreadLocal<Deque<String>>LOOKUP_KEY_HOLDER=ThreadLocal.withInitial(ArrayDeque::new);publicstaticvoidpush(Stringds){LOOKUP_KEY_HOLDER.get().push(ds);}publicstaticvoidpoll(){Deque<String>deque=LOOKUP_KEY_HOLDER.get();deque.poll();if(deque.isEmpty()){LOOKUP_KEY_HOLDER.remove();}}publicstaticStringpeek(){returnLOOKUP_KEY_HOLDER.get().peek();}}

栈结构的意义:支持嵌套调用场景。例如方法 A 使用@DS("master"),其内部调用方法 B 使用@DS("slave"),执行 B 时压入 “slave”,B 结束后弹出恢复 “master”。

(3)路由数据源
publicclassDynamicRoutingDataSourceextendsAbstractRoutingDataSource{// 存储所有数据源实例privateMap<String,DataSource>dataSourceMap;@OverrideprotectedObjectdetermineCurrentLookupKey(){// 从 ThreadLocal 获取当前数据源标识returnDynamicDataSourceContextHolder.peek();}@OverrideprotectedDataSourcedetermineTargetDataSource(){StringlookupKey=(String)determineCurrentLookupKey();DataSourcedataSource=dataSourceMap.get(lookupKey);if(dataSource==null){thrownewDataSourceNotFoundException("数据源 ["+lookupKey+"] 未找到");}returndataSource;}}
(4)注解解析器
publicclassDynamicDataSourceClassResolver{publicStringfindKey(Methodmethod,Class<?>targetClass){// 优先级:方法 > 类 > 接口方法 > 接口类// 1. 检查方法上的 @DSDSds=method.getAnnotation(DS.class);if(ds!=null)returnds.value();// 2. 检查类上的 @DSds=targetClass.getAnnotation(DS.class);if(ds!=null)returnds.value();// 3. 检查接口方法上的 @DS// 4. 检查接口类上的 @DS// ...// 5. 返回默认数据源return"master";}}

四、与@Transactional的协同与冲突

4.1 典型用法

@DS("warehouse")@Transactional(propagation=Propagation.REQUIRES_NEW)publicclassOrderDaoImplextendsServiceImpl<...>{}

4.2 执行顺序问题

Spring 事务管理器和@DSAOP 都基于代理,执行顺序取决于@Order

顺序效果
事务 AOP 先执行@Order更小)事务先绑定默认数据源的 Connection,@DS切换无效
@DSAOP 先执行@Order更小)先切换数据源,事务在正确数据源上开启

dynamic-datasource-spring-boot-starter默认将@DS的 AOP 优先级设为Ordered.HIGHEST_PRECEDENCE,确保先切换数据源,再开启事务

4.3REQUIRES_NEW的作用

Propagation.REQUIRES_NEW表示始终开启新事务,挂起外层事务。结合@DS使用时:

  • 外层事务使用数据源 A
  • 进入OrderDaoImpl时,@DS先切换到warehouse数据源
  • REQUIRES_NEWwarehouse数据源上开启新事务
  • 方法结束后,新事务提交,数据源恢复,外层事务继续

如果不用REQUIRES_NEW而用默认的REQUIRED:外层事务已绑定了 master 数据源的 Connection,@DS切换可能不生效(因为事务同步管理器已绑定 Connection)。


五、完整调用时序图

Caller │ ├─① 调用 @DS("warehouse") 标注的方法 │ ▼ DynamicDataSourceAnnotationInterceptor.invoke() │ ├─② findKey() → 解析 @DS 注解 → 返回 "warehouse" │ ├─③ DynamicDataSourceContextHolder.push("warehouse") │ ThreadLocal 栈: ["warehouse"] │ ├─④ Spring TransactionInterceptor.invoke() │ │ │ ├─⑤ DynamicRoutingDataSource.determineTargetDataSource() │ │ 从 ThreadLocal 取 "warehouse" → 返回对应 DataSource │ │ │ ├─⑥ 从 warehouse DataSource 获取 Connection │ │ │ ├─⑦ 开启事务 (REQUIRES_NEW → 新事务) │ │ │ ├─⑧ 执行 MyBatis SQL → 使用 warehouse 连接 │ │ │ └─⑨ 提交/回滚事务 │ ├─⑩ DynamicDataSourceContextHolder.poll() │ ThreadLocal 栈: [] (恢复为空) │ └─⑪ 返回结果

六、关键设计要点总结

要点说明
AOP 拦截基于MethodInterceptor拦截@DS标注的方法
ThreadLocal 栈使用栈结构存储数据源标识,支持嵌套调用场景
AbstractRoutingDataSource继承 Spring 的路由数据源抽象类,运行时动态选择数据源
注解优先级方法级 > 类级 > 接口方法级 > 接口类级 > 默认 master
AOP 顺序@DS优先级最高,确保先切换数据源再开启事务
事务配合需注意@Transactional@DS的 AOP 顺序,REQUIRES_NEW可避免事务绑定冲突
连接池隔离每个数据源拥有独立的连接池(HikariCP/Druid),互不影响

七、数据源配置参考

定义数据源常量:

publicclassDsName{publicstaticfinalStringMASTER="master";// 主库publicstaticfinalStringSLAVE="slave";// 只读库publicstaticfinalStringSTAR_ROCKS="starrocks";// StarRocks 分析库publicstaticfinalStringWAREHOUSE="warehouse";// 数据仓库}

对应application.yml配置:

spring:datasource:dynamic:primary:masterdatasource:master:url:jdbc:mysql://host:3306/db_mainusername:xxxpassword:xxxslave:url:jdbc:mysql://host:3306/db_slavestarrocks:url:jdbc:mysql://host:9030/db_analyticswarehouse:url:jdbc:mysql://host:3306/db_warehouse

八、常见问题与最佳实践

8.1@DS不生效的常见原因

原因解决方案
同类内部方法调用(绕过代理)@DS方法抽到独立 Bean,或使用AopContext.currentProxy()
@TransactionalAOP 优先级高于@DS确认dynamic-datasource版本 ≥ 3.x,或手动指定@Order
在非 Spring 管理的线程中使用手动调用DynamicDataSourceContextHolder.push()/poll()
@DS标注在 private 方法上AOP 无法拦截 private 方法,需改为 public 或提升到类级别

8.2 最佳实践

  1. @DS优先标注在类上:减少遗漏,整个类默认使用指定数据源
  2. 方法级@DS覆盖类级:个别需要切换数据源的方法单独标注
  3. 配合REQUIRES_NEW使用:跨数据源调用时避免事务绑定冲突
  4. 避免同类内部调用:确保 AOP 代理生效
  5. 嵌套@DS调用:栈结构自动处理,无需手动管理
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/21 7:31:29

PCA在单细胞计数数据中的失效边界与替代方案

1. 项目概述&#xff1a;当生物信息学的“万能钥匙”开始生锈 在生物信息学实验室里&#xff0c;PCA&#xff08;主成分分析&#xff09;几乎像离心机一样常见——它被默认安装在每台工作站上&#xff0c;写进每份RNA-seq分析流程的脚本开头&#xff0c;出现在90%以上的单细胞转…

作者头像 李华
网站建设 2026/7/21 7:30:53

只会喷不会做,数字孪生不养嘴炮。

我干这行十年了&#xff0c;见过太多人。有一种人我特别烦——就是那种什么都不会做&#xff0c;但什么都敢说的人。你辛辛苦苦搭了一个数字孪生场景&#xff0c;光照调了两天&#xff0c;模型面数优化了好几轮&#xff0c;交互逻辑写了小半个月&#xff0c;好不容易跑通了。他…

作者头像 李华
网站建设 2026/7/21 7:27:38

Codex自我控制功能解析:AI代码生成的安全保障机制与实践

这次我们来看一个关于 Codex 自我控制功能提醒的项目。Codex 作为 OpenAI 的重要模型&#xff0c;在代码生成和自然语言处理方面有着广泛应用&#xff0c;而自我控制功能则是确保模型使用安全性和合规性的关键机制。 这个功能的核心价值在于让 Codex 能够自主识别和规避可能涉…

作者头像 李华
网站建设 2026/7/21 7:20:30

BlueScreenView免安装使用指南:5分钟定位Windows蓝屏元凶

前言 相信每个Windows用户都曾有过这样的经历&#xff1a;正专心致志地写文档、打游戏或处理工作&#xff0c;屏幕突然一蓝&#xff0c;伴随着一串让人心慌的白色代码&#xff0c;电脑就这么毫无征兆地崩溃了。很多人遇到蓝屏的第一反应就是重启&#xff0c;祈祷它别再发生。但…

作者头像 李华
网站建设 2026/7/21 7:19:54

劳力士2026新版官方保养政策解析与实操指南

1. 劳力士官方维修保养2026新版解析 作为钟表行业的标杆品牌&#xff0c;劳力士的售后服务体系一直保持着严苛的标准。2026年最新修订的官方保养政策在保持核心服务框架的同时&#xff0c;对部分细节进行了优化调整。根据我在高端腕表维修行业12年的从业经验&#xff0c;这次更…

作者头像 李华