【多数据源系列】在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类图如下图所示:

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

1.2.3.  AbstractRoutingDataSource类结构

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    private Map 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. 父模块依赖

  
        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
            
        
    

 2.2.2 数据源切换模块

	
		
			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
		
	

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 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 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博客

你可能感兴趣的:(JAVA,spring,cloud,spring,boot,java)