本文将以代码示例介绍在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<Object, Object> targetDataSources;private Object defaultTargetDataSource;private boolean lenientFallback = true;private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();// 省略其他成员变量和方法protected abstract Object determineCurrentLookupKey();@Overridepublic Connection getConnection() throws SQLException {return determineTargetDataSource().getConnection();}@Overridepublic 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() {Null(this.targetDataSources, "TargetDataSources property must be set");Object lookupKey = determineCurrentLookupKey();DataSource dataSource = (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依赖包含两个模块的依赖内容,即父模块和数据源切换模块。
<properties><mavenpiler.source>8</mavenpiler.source><mavenpiler.target>8</mavenpiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>&porting.outputEncoding>UTF-8</porting.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>
<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&acle</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>
@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 blocke.printStackTrace();}}//2.获取连接public Connection getConnection() {Connection conn = null;try {conn = Connection(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 blocke.printStackTrace();}}//关闭statementif (st != null) {try {st.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}//关闭结果集if (rs != null) {try {rs.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.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{@Autowiredprivate 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 = Connection();statement = ateStatement();String sql = "select * from initialization_data_source";resultSet = uteQuery(sql);Map<Object, Object> targetDataSources = new HashMap<>();//遍历循环while (()) {//数据库urlString url = String("url");//用户名String userName = String("user_name");//密码String password = String("password");//数据源名称String connection_name = String("connection_name");//驅動String driverClassName= String("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 {//释放资源leaseResc(resultSet, statement, connection);}return null;}
}
上述代码的作用为在项目启动时读取 initialization_data_source指定初始数据源(connection_name为master)。
initialization_data_source我上传到我的资源里了,需要的同学可以自行去下载=1001.2014.3001.5503
@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() {();}public static void clearDataSource() {ve();}
}
定义ThreadLocal,通过setDataSource(value)函数指定数据源标识key,AbstractRoutingDataSource会根据当前线程的标识去映射表中查找对应的数据源,完成数据源切换。
public class MultiRouteDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {//通过绑定线程的数据源上下文实现多数据源的动态切换DataSource();}}
完成上述代码后仅需要将DataSourceContext注入到需要做代码切换的地方,即可通过setDataSource(String value)切换数据源(ps:只能在controller中切换),记得在末尾要执行clearDataSource(),否则会造成内存泄露。
SpringBoot——动态数据源(多数据源自动切换)-CSDN博客
SpringBoot 动态配置数据源_为什么需要动态数据源-CSDN博客
本文发布于:2024-01-28 22:13:39,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170645122610654.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |