【多数据源系列】在Sping Cloud(Spring Boot)中基于AbstractRoutingDataSource 实现多数据源动态切换

  本文将以代码示例介绍在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. 参考链接 


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 Boot2.6.3
Spring Cloud Alibaba2021.0.1.0
Spring Cloud 2021.0.1
java1.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.csdn.net/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(),否则会造成内存泄露。

4. 参考链接 

SpringBoot——动态数据源(多数据源自动切换)-CSDN博客

SpringBoot 动态配置数据源_为什么需要动态数据源-CSDN博客

  • 24
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring BootSpring Cloud是两个不同的框架,它们都是基于Spring Framework的。Spring Boot主要是用于快速构建单个微服务,而Spring Cloud则是用于构建分布式系统的多个微服务。 下面是Spring CloudSpring Boot的优缺点比较: Spring Boot优点: 1. 简单易用:Spring Boot提供了自动配置和快速开发的能力,使得开发人员可以快速地构建和运行Spring应用程序。 2. 强大的开发工具:Spring Boot提供了强大的开发工具,比如Spring Boot CLI和Spring Boot Starter,使得开发人员可以轻松地创建和部署应用程序。 3. 集成了大量的第三方库:Spring Boot集成了大量的第三方库,比如Hibernate、Thymeleaf、Jackson等,使得开发人员可以轻松地使用这些库来处理不同的业务需求。 4. 适用于小型应用程序:Spring Boot适用于构建小型的应用程序,可以快速地实现业务需求。 Spring Boot缺点: 1. 不适用于大型应用程序:Spring Boot不适用于构建大型的应用程序,因为它缺乏分布式系统的支持。 2. 缺乏服务治理:Spring Boot缺乏服务治理的能力,因此在构建分布式系统时需要使用Spring CloudSpring Cloud优点: 1. 服务治理:Spring Cloud提供了服务注册、发现、负载均衡和断路器等服务治理功能,使得开发人员可以构建分布式系统的多个微服务。 2. 分布式配置管理:Spring Cloud提供了分布式配置管理的功能,可以快速地更新应用程序的配置。 3. 分布式追踪:Spring Cloud提供了分布式追踪的功能,可以快速地定位应用程序的异常。 Spring Cloud缺点: 1. 复杂度高:Spring Cloud的复杂度很高,需要开发人员具备分布式系统的技术知识。 2. 性能问题:Spring Cloud会增加系统的复杂度,可能会影响系统的性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后端小肥肠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值