本文将以代码示例介绍在Spring Cloud中基于AbstractRoutingDataSource实现多数据源动态切换。
- 如文章中有明显错误或者用词不当的地方,欢迎大家在评论区批评指正,我看到后会及时修改。
如想要和博主进行技术栈方面的讨论和交流可私信我。
目录
1.2.3. AbstractRoutingDataSource类结构
1. 前言
1.1. 背景
在近几年的业务需求中,我碰到了几个需要支持动态数据源切换的需求场景,如数据库读写优化,后台改为读写分离;需要在一个界面中同时支持读取不同数据库的数据(如Postgres和Oracle)。
以在一个界面中同时支持读取不同数据库的数据这一需求为例,要实现这一功能可以用微服务走远程调用解决,但是一个界面通常属于一类业务,一般我是不会在往下拆分模块的(我个人习惯是一类业务对应一个微服务模块如用户模块,审批模块,鉴权模块),故考虑到了使用动态切换数据源来实现这个功能需求,网上找了很多解决方案最终选择了AbstractRoutingDataSource 。
1.2. 原理
1.2.1 核心原理
AbstractRoutingDataSource是 Spring Framework 中提供的一个抽象类,用于支持动态切换数据源,它的原理是运行时动态地确定当前线程应该使用哪个数据源。其中几个核心原理如下:
1. 数据源映射
AbstractRoutingDataSource内部维护了一个数据源的映射表。这个映射表将一个标识(通常是一个线程本地变量)映射到具体的数据源。
2. 决定数据源
在每次数据库操作之前,AbstractRoutingDataSource会根据当前线程的标识去映射表中查找对应的数据源。这个标识通常存储在一个线程本地变量中,确保每个线程都可以拥有自己的数据源。
3. 线程本地变量
Spring 通常使用ThreadLocal 存储当前线程的上下文信息。在多线程环境中,每个线程都可以拥有自己的线程本地变量,这确保了线程间的数据隔离。
4. 切换数据源
在执行数据库操作之前,AbstractRoutingDataSource 会通过线程本地变量找到当前线程应该使用的数据源,并在运行时切换到该数据源。
1.2.2. 源码解析
AbstractRoutingDataSource类图如下图所示:
1.2.3. AbstractRoutingDataSource类结构
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
// 省略其他成员变量和方法
protected abstract Object determineCurrentLookupKey();
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
// 省略其他方法
}
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;
}
2. 开发环境搭建
2.1. 所用版本工具
依赖 | 版本 |
---|---|
Spring Boot | 2.6.3 |
Spring Cloud Alibaba | 2021.0.1.0 |
Spring Cloud | 2021.0.1 |
java | 1.8 |
2.2. pom依赖
pom依赖包含两个模块的依赖内容,即父模块和数据源切换模块。
2.2.1. 父模块依赖
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.1</spring-cloud.version>
<cloud-alibaba.version>2021.0.1.0</cloud-alibaba.version>
<spring-boot.version>2.6.3</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- springCloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.2.2 数据源切换模块
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc8</artifactId>
<version>12.2.0.1.0</version>
</dependency>
<!--热部署 ctrl+f9-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
3. 核心代码编写
3.1. 编写JDBCUtil
@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();
}
}
}
3.2. 编写DataSourceComponent
@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<Object, Object> targetDataSources = new HashMap<>();
//遍历循环
while (resultSet.next()) {
//数据库url
String url = resultSet.getString("url");
//用户名
String userName = resultSet.getString("user_name");
//密码
String password = resultSet.getString("password");
//数据源名称
String connection_name = resultSet.getString("connection_name");
//驅動
String driverClassName= resultSet.getString("driver_class_name");
//创建Hikari数据库连接池
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(url);
dataSource.setUsername(userName);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverClassName);
//Hikari数据池的配置
dataSource.addDataSourceProperty("initialSize",8);
dataSource.addDataSourceProperty("minIdle",5);
dataSource.addDataSourceProperty("maxActive",20);
dataSource.addDataSourceProperty("maxWait",60000);
dataSource.addDataSourceProperty("timeBetweenEvictionRunsMillis",60000);
dataSource.addDataSourceProperty("minEvictableIdleTimeMillis",300000);
//把datasource放入map 多数据源每个key对应一个数据源
targetDataSources.put(connection_name,dataSource);
//数据库留有一条主数据源
if(connection_name.equals("master")){
//把此主数据源设置为默认加载
multiDataSource.setDefaultTargetDataSource(dataSource);
}
}
// 设置多数据源. key value的形式
multiDataSource.setTargetDataSources(targetDataSources);
return multiDataSource;
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放资源
jdbcUtil.releaseResc(resultSet, statement, connection);
}
return null;
}
}
上述代码的作用为在项目启动时读取 initialization_data_source指定初始数据源(connection_name为master)。
initialization_data_source我上传到我的资源里了,需要的同学可以自行去下载https://download.youkuaiyun.com/download/c18213590220/88625808?spm=1001.2014.3001.5503
3.3. 编写DataSourceContext
@Component
public class DataSourceContext {
private static final ThreadLocal<String> 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会根据当前线程的标识去映射表中查找对应的数据源,完成数据源切换。
3.4. 编写 MultiRouteDataSource
public class MultiRouteDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
//通过绑定线程的数据源上下文实现多数据源的动态切换
return DataSourceContext.getDataSource();
}
}
完成上述代码后仅需要将DataSourceContext注入到需要做代码切换的地方,即可通过setDataSource(String value)切换数据源(ps:只能在controller中切换),记得在末尾要执行clearDataSource(),否则会造成内存泄露。