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 | 推荐使用的封装库 |