多数据源切换:@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 | 注解,标记目标数据源名称 |
DynamicDataSourceAnnotationInterceptor | AOP 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_NEW在warehouse数据源上开启新事务- 方法结束后,新事务提交,数据源恢复,外层事务继续
如果不用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 最佳实践
@DS优先标注在类上:减少遗漏,整个类默认使用指定数据源- 方法级
@DS覆盖类级:个别需要切换数据源的方法单独标注 - 配合
REQUIRES_NEW使用:跨数据源调用时避免事务绑定冲突 - 避免同类内部调用:确保 AOP 代理生效
- 嵌套
@DS调用:栈结构自动处理,无需手动管理