当前位置: 首页 > news >正文

使用 Spring Boot + AbstractRoutingDataSource 实现动态切换数据源

1. 动态切换数据源的原理

AbstractRoutingDataSource 是 Spring 提供的一个抽象类,它通过实现 determineCurrentLookupKey 方法,根据上下文信息决定当前使用的数据源。核心流程如下:

  • 定义多数据源配置:注册多个数据源。
  • 实现动态数据源路由:继承 AbstractRoutingDataSource,根据上下文返回数据源标识。
  • 使用拦截器设置上下文:在请求中设置当前使用的数据源。

 2. 实现步骤

2.1 确保你的 pom.xml 中已经包含如下依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope>
</dependency>

 

2.2 继承自 Spring 提供的抽象类 AbstractRoutingDataSource
package com.imooc.cloud.springboot;import com.imooc.cloud.dynamic.raw.DataSourceContext;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import java.util.Map;public class SpringDynamicDataSource extends AbstractRoutingDataSource {public SpringDynamicDataSource(Map<Object, Object> targetDataSources) {super.setTargetDataSources(targetDataSources);}@Overrideprotected Object determineCurrentLookupKey() {return DataSourceContext.getCurrentDb();}
}

类定义

public class SpringDynamicDataSource extends AbstractRoutingDataSource {
}
  • 继承自 Spring 提供的抽象类 AbstractRoutingDataSource
  • 是实现多数据源切换的核心类。

构造函数

public SpringDynamicDataSource(Map<Object, Object> targetDataSources) {super.setTargetDataSources(targetDataSources);
}
  • 通过构造器传入多个目标数据源(通常是 Map<标识符, DataSource> 形式)。
  • 调用父类方法设置这些数据源。

核心方法:determineCurrentLookupKey()

@Override
protected Object determineCurrentLookupKey() {return DataSourceContext.getCurrentDb();
}
  • Spring 框架会在每次数据库操作时调用这个方法。
  • 返回当前线程使用的数据源标识(如 "master""slave1")。
  • 实际上是从 ThreadLocal 中获取当前线程绑定的数据源名称。

2.3 数据源上下文工具类 
public class DataSourceContext {private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();public static void setCurrentDb(String db) {CONTEXT.set(db);}public static String getCurrentDb() {return CONTEXT.get();}public static void removeCurrentDb() {CONTEXT.remove();}
}

用于保存和清除当前线程使用的数据源标识。


2.4 将多数据源注入并创建 SpringDynamicDataSource
package com.imooc.cloud.springboot;import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;public class SpringDataSourceConfiguration {@Beanpublic DataSource mybatisPlusDataSource() {return DataSourceBuilder.create().driverClassName("com.mysql.jdbc.Driver").url("jdbc:mysql://192.168.3.150:3306/mybatisplus?characterEncoding=utf8").username("root").password("123456").build();}@Beanpublic DataSource mybatisExampleDataSource() {return DataSourceBuilder.create().driverClassName("com.mysql.jdbc.Driver").url("jdbc:mysql://192.168.3.150:3306/mybatis-example?characterEncoding=utf8").username("root").password("123456").build();}@Primary@Beanpublic SpringDynamicDataSource springDynamicDataSource() {Map<Object, Object> targetDataSources = new HashMap<>();DataSource mybatisPlusDataSource = mybatisPlusDataSource();DataSource mybatisExampleDataSource = mybatisExampleDataSource();targetDataSources.put("mybatisPlus", mybatisPlusDataSource);targetDataSources.put("mybatisExample", mybatisExampleDataSource);return new SpringDynamicDataSource(targetDataSources);}
}
2.5 安全地保存和切换当前线程使用的数据源

在多线程环境下,安全地保存和切换当前线程使用的数据源标识(如 "master""slave1" 等),支持嵌套调用(例如在事务中嵌套切换数据源),并且使用 双端队列(Deque)模拟栈结构 来管理数据源切换的上下文。 

  • 使用 ThreadLocal 保存每个线程独立的 数据源栈(Deque)
  • 使用 NamedThreadLocal 有助于在调试或日志中识别该线程局部变量的用途。
  • ArrayDeque 是一个双端队列,这里用作栈(LIFO),实现嵌套切换数据源的功能。
package com.imooc.cloud.util;import org.springframework.core.NamedThreadLocal;
import org.springframework.util.StringUtils;import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;public final class DynamicDataSourceContextHolder {/*** 双端队列其实本质就是一个栈*/private static final ThreadLocal<Deque<String>> DATASOURCE_CONTEXT = NamedThreadLocal.withInitial(() -> new ArrayDeque<>());private DynamicDataSourceContextHolder() {if (DATASOURCE_CONTEXT != null) {throw new RuntimeException("禁止反射创建");}}public static String getCurrentDataSource() {//todo 2023-07-31 修复补丁。因为可能返回null,而ConcurrentHashMap的get方法不能传入null,否则报空指针String peek = DATASOURCE_CONTEXT.get().peek();return Objects.isNull(peek) ? "" : peek;}public static String addDataSource(String dds) {String datasource = StringUtils.isEmpty(dds) ? "" : dds;DATASOURCE_CONTEXT.get().push(datasource);return datasource;}public static void removeCurrentDataSource() {Deque<String> deque = DATASOURCE_CONTEXT.get();deque.poll();if (deque.isEmpty()) {DATASOURCE_CONTEXT.remove();}}
}
单例构造限制
private DynamicDataSourceContextHolder() {if (DATASOURCE_CONTEXT != null) {throw new RuntimeException("禁止反射创建");}
}
  • 私有构造方法,防止外部实例化。
  • 添加了反射创建检测,防止通过反射破坏单例。
获取当前数据源
public static String getCurrentDataSource() {String peek = DATASOURCE_CONTEXT.get().peek();return Objects.isNull(peek) ? "" : peek;
}
  • 从当前线程的数据源栈中获取当前使用的数据源标识。
  • 如果栈为空,返回空字符串 "",避免后续操作(如 Map.get(null))导致空指针异常。
设置新数据源(入栈)
public static String addDataSource(String dds) {String datasource = StringUtils.isEmpty(dds) ? "" : dds;DATASOURCE_CONTEXT.get().push(datasource);return datasource;
}
  • 将指定的数据源标识压入栈顶。
  • 支持嵌套切换数据源(例如 AOP + 事务中嵌套注解切换)。
  • 如果传入 null 或空字符串,则使用默认空字符串。
 移除当前数据源(出栈)
public static void removeCurrentDataSource() {Deque<String> deque = DATASOURCE_CONTEXT.get();deque.poll();if (deque.isEmpty()) {DATASOURCE_CONTEXT.remove();}
}
  • 从栈中弹出一个数据源标识(LIFO)。
  • 如果栈为空,则清除整个线程局部变量,防止内存泄漏。

这个工具类通常用于配合 动态数据源路由类(如 AbstractRoutingDataSource)一起使用,实现多数据源切换。例如:

1. 动态数据源路由类(简化版) 

public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.getCurrentDataSource();}
}

2. AOP 切面控制数据源切换

@Aspect
@Component
public class DataSourceAspect {@Before("@annotation(ds))")public void beforeSwitchDS(JoinPoint point, DynamicDataSource ds) {DynamicDataSourceContextHolder.addDataSource(ds.db());}@After("@annotation(ds))")public void afterSwitchDS(JoinPoint point, DynamicDataSource ds) {DynamicDataSourceContextHolder.removeCurrentDataSource();}
}

3. 注解定义

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicDataSource {String db() default "master";
}

4. Service 使用示例

@Service
public class UserService {@DynamicDataSource("slave1")public List<User> queryFromSlave() {return userMapper.selectAll();}public void insertUser(User user) {userMapper.insert(user);}
}


3. 测试

package com.imooc.cloud;import com.imooc.cloud.util.DynamicDataSourceContextHolder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;import java.util.List;@SpringBootTest
public class SpringDynamicTest {@Autowiredprivate JdbcTemplate jdbcTemplate;@Testpublic void testQueryUser() {DynamicDataSourceContextHolder.addDataSource("mybatisPlus");List list = jdbcTemplate.queryForList("select * from user");System.out.println("list: "+list);}@Testpublic void testQueryOrder() {DynamicDataSourceContextHolder.addDataSource("mybatisExample");List list = jdbcTemplate.queryForList("select * from `user`");System.out.println("list: "+list);}
}

4. 完整使用流程图

+-----------------+
| @DynamicDataSource("slave1") |
+-----------------+↓
+----------------------+
| AOP Before Advice    |
| DynamicDataSourceContextHolder.addDataSource("slave1") |
+----------------------+↓
+----------------------+
| AbstractRoutingDataSource.determineCurrentLookupKey() |
| return DynamicDataSourceContextHolder.getCurrentDataSource() |
+----------------------+↓
+----------------------+
| JDBC / MyBatis 使用对应数据源执行 SQL |
+----------------------+↓
+----------------------+
| AOP After Advice     |
| DynamicDataSourceContextHolder.removeCurrentDataSource() |
+----------------------+

5. 推荐使用 dynamic-datasource-spring-boot-starter

新项目,强烈建议使用开源组件来简化多数据源配置:

1. 引入依赖
<dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>4.2.0</version>
</dependency>
2. 配置文件(application.yml)
spring:datasource:dynamic:primary: masterdatasource:master:url: jdbc:mysql://localhost:3306/masterusername: rootpassword: rootslave1:url: jdbc:mysql://localhost:3306/slave1username: rootpassword: root
3. 使用注解
@DS("slave1")
public List<User> queryFromSlave() {return userMapper.selectList(null);
}

🎯 总结

功能说明
DynamicDataSourceContextHolder数据源上下文管理工具
Deque<String>支持嵌套切换
ThreadLocal线程隔离
AOP + 注解实现优雅的数据源切换
dynamic-datasource-spring-boot-starter推荐使用的封装库
http://www.xdnf.cn/news/1135999.html

相关文章:

  • 高光谱相机有多少种类型?分别有什么特点?
  • Java面试(基础篇) - 第二篇!
  • 2020717零碎写写
  • 91套商业策划创业融资计划书PPT模版
  • Matlab2025a软件安装|详细安装步骤➕安装文件|附下载文件
  • IDEA运行Tomcat一直提示端口被占用(也查不到该端口)
  • 社区搜索离线回溯系统设计:架构、挑战与性能优化|得物技术
  • 在开关电源电路中,WD0407 可作为整流二极管使用,WD0407 40V 7A
  • 前端-列表fixed冻结的列 横向滚动条拖不动
  • NTC电阻防浪涌介绍
  • Linux内核网络栈深度剖析:inet_connection_sock.c的服务器端套接字管理
  • Flutter:上传图片,选择相机或相册:wechat_assets_picker
  • 【软件开发】Copilot 编码插件
  • 【网易云-body1】
  • TRAE IDE** 下载、安装、开发、测试和部署 2048 小游戏的全流程指南
  • 界面控件Kendo UI for Angular 2025 Q2新版亮点 - 增强跨设备的无缝体验
  • 杨耀东老师在ICML2025上对齐教程:《语言模型的对齐方法:一种机器学习视角》
  • 《工程伦理》分析报告五 软件开发
  • Vue3入门-计算属性+监听器
  • Vmware虚拟机使用仅主机模式共享物理网卡访问互联网
  • 时序数据库选型指南 —— 为什么选择 Apache IoTDB?
  • Linux中的数据库操作基础
  • ros2 标定相机
  • Qwen3-8B Dify RAG环境搭建
  • 2D视觉系统标定流程与关键要求
  • 高光谱相机(Hyperspectral Camera)
  • 【后端】Linux系统发布.NetCore项目
  • 尺寸标注识别3 实例分割 roboflow
  • NumPy, SciPy 之间的区别
  • 大语言模型任务分解与汇总:从认知瓶颈到系统化解决方案