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);
}
@Override
protected 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 {
@Bean
public 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();
}
@Bean
public 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
@Bean
public 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 {
@Override
protected 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 {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void testQueryUser() {
DynamicDataSourceContextHolder.addDataSource("mybatisPlus");
List list = jdbcTemplate.queryForList("select * from user");
System.out.println("list: "+list);
}
@Test
public 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: master
datasource:
master:
url: jdbc:mysql://localhost:3306/master
username: root
password: root
slave1:
url: jdbc:mysql://localhost:3306/slave1
username: root
password: root
3. 使用注解
@DS("slave1")
public List<User> queryFromSlave() {
return userMapper.selectList(null);
}
🎯 总结
功能 | 说明 |
---|---|
DynamicDataSourceContextHolder | 数据源上下文管理工具 |
Deque<String> | 支持嵌套切换 |
ThreadLocal | 线程隔离 |
AOP + 注解 | 实现优雅的数据源切换 |
dynamic-datasource-spring-boot-starter | 推荐使用的封装库 |