本文将以代码示例介绍在Spring Cloud中基于AbstractRoutingDataSource实现多数据源动态切换。
- 如文章中有明显错误或者用词不当的地方,欢迎大家在评论区批评指正,我看到后会及时修改。
如想要和博主进行技术栈方面的讨论和交流可私信我。
目录
1. 前言
1.1. 背景
1.2. 原理
1.2.1 核心原理
1.2.2. 源码解析
1.2.3. AbstractRoutingDataSource类结构
2. 开发环境搭建
2.1. 所用版本工具
2.2. pom依赖
2.2.1. 父模块依赖
2.2.2 数据源切换模块
3. 核心代码编写
3.1. 编写JDBCUtil
3.2. 编写DataSourceComponent
3.3. 编写DataSourceContext
3.4. 编写 MultiRouteDataSource
4. 参考链接
在近几年的业务需求中,我碰到了几个需要支持动态数据源切换的需求场景,如数据库读写优化,后台改为读写分离;需要在一个界面中同时支持读取不同数据库的数据(如Postgres和Oracle)。
以在一个界面中同时支持读取不同数据库的数据这一需求为例,要实现这一功能可以用微服务走远程调用解决,但是一个界面通常属于一类业务,一般我是不会在往下拆分模块的(我个人习惯是一类业务对应一个微服务模块如用户模块,审批模块,鉴权模块),故考虑到了使用动态切换数据源来实现这个功能需求,网上找了很多解决方案最终选择了AbstractRoutingDataSource 。
AbstractRoutingDataSource是 Spring Framework 中提供的一个抽象类,用于支持动态切换数据源,它的原理是运行时动态地确定当前线程应该使用哪个数据源。其中几个核心原理如下:
1. 数据源映射
AbstractRoutingDataSource内部维护了一个数据源的映射表。这个映射表将一个标识(通常是一个线程本地变量)映射到具体的数据源。
2. 决定数据源
在每次数据库操作之前,AbstractRoutingDataSource会根据当前线程的标识去映射表中查找对应的数据源。这个标识通常存储在一个线程本地变量中,确保每个线程都可以拥有自己的数据源。
3. 线程本地变量
Spring 通常使用ThreadLocal 存储当前线程的上下文信息。在多线程环境中,每个线程都可以拥有自己的线程本地变量,这确保了线程间的数据隔离。
4. 切换数据源
在执行数据库操作之前,AbstractRoutingDataSource 会通过线程本地变量找到当前线程应该使用的数据源,并在运行时切换到该数据源。
AbstractRoutingDataSource类图如下图所示:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map
1. determineCurrentLookupKey方法
determineCurrentLookupKey是一个抽象方法,它由具体的子类实现。这个方法的目的是确定当前线程应该使用的数据源的标识。在实际应用中,这个方法通常通过访问线程本地变量或其他上下文信息来获取标识。
2. getConnection
方法
getConnection
方法是从 AbstractDataSource
继承而来的,它在每次获取连接时调用 determineTargetDataSource
方法来确定当前应该使用的数据源,然后返回该数据源的连接。
3. determineTargetDataSource
方法
determineTargetDataSource
方法根据 determineCurrentLookupKey
的返回值选择目标数据源。如果找不到对应的数据源,则使用默认的数据源。
protected DataSource determineTargetDataSource() {
Assert.notNull(this.targetDataSources, "TargetDataSources property must be set");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.targetDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.defaultTargetDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
依赖 | 版本 |
---|---|
Spring Boot | 2.6.3 |
Spring Cloud Alibaba | 2021.0.1.0 |
Spring Cloud | 2021.0.1 |
java | 1.8 |
pom依赖包含两个模块的依赖内容,即父模块和数据源切换模块。
8
8
UTF-8
UTF-8
1.8
2021.0.1
2021.0.1.0
2.6.3
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${cloud-alibaba.version}
pom
import
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
org.springframework.cloud
spring-cloud-starter-loadbalancer
org.springframework.boot
spring-boot-starter-test
test
org.postgresql
postgresql
com.oracle
ojdbc8
12.2.0.1.0
org.springframework.boot
spring-boot-devtools
org.projectlombok
lombok
@Data
@Component
@RefreshScope
public class JDBCUtil {
@Value("${primary-datasource.url}")
private String url;
@Value("${primary-datasource.user}")
private String user;
@Value("${primary-datasource.password}")
private String password;
//1.加载驱动
static {
try {
Class.forName("org.postgresql.Driver");
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//2.获取连接
public Connection getConnection() {
Connection conn = null;
try {
conn = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
//3.关闭连接
public void close(Connection conn, Statement st, ResultSet rs) {
//关闭连接
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//关闭statement
if (st != null) {
try {
st.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//关闭结果集
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void releaseResc(ResultSet resultSet, Statement statement, Connection connection) {
try {
if (resultSet != null && !resultSet.isClosed()) {
resultSet.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (statement != null && !statement.isClosed()) {
statement.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (connection != null && !connection.isClosed()) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Configuration
public class DataSourceComponent{
@Autowired
private JDBCUtil jdbcUtil;
@Primary//表示优先被注入
@Bean(name = "multiDataSource")
public MultiRouteDataSource exampleRouteDataSource() {
MultiRouteDataSource multiDataSource = new MultiRouteDataSource();
ResultSet resultSet = null;
Statement statement = null;
Connection connection = null;
try {
//采用jdbc访问主数据库
connection = jdbcUtil.getConnection();
statement = connection.createStatement();
String sql = "select * from initialization_data_source";
resultSet = statement.executeQuery(sql);
Map
上述代码的作用为在项目启动时读取 initialization_data_source指定初始数据源(connection_name为master)。
initialization_data_source我上传到我的资源里了,需要的同学可以自行去下载https://download.csdn.net/download/c18213590220/88625808?spm=1001.2014.3001.5503
@Component
public class DataSourceContext {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
public static void setDataSource(String value) {
contextHolder.set(value);
}
public static String getDataSource() {
return contextHolder.get();
}
public static void clearDataSource() {
contextHolder.remove();
}
}
定义ThreadLocal,通过setDataSource(value)函数指定数据源标识key,AbstractRoutingDataSource会根据当前线程的标识去映射表中查找对应的数据源,完成数据源切换。
public class MultiRouteDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
//通过绑定线程的数据源上下文实现多数据源的动态切换
return DataSourceContext.getDataSource();
}
}
完成上述代码后仅需要将DataSourceContext注入到需要做代码切换的地方,即可通过setDataSource(String value)切换数据源(ps:只能在controller中切换),记得在末尾要执行clearDataSource(),否则会造成内存泄露。
SpringBoot——动态数据源(多数据源自动切换)-CSDN博客
SpringBoot 动态配置数据源_为什么需要动态数据源-CSDN博客