Springcloud学习笔记57--Spring 多数据源切换

1 环境准备

pom文件

复制代码
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
        <relativePath/>
    </parent>
 
    <dependencies>
        <!-- druid数据源驱动 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis SpringBoot依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>compile</scope>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
 
        <!-- aop依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <!-- 通用mapper -->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>1.1.5</version>
        </dependency>
        <!-- druid监控依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.28</version>
        </dependency>
    </dependencies>
复制代码

2. DynamicDataSource 动态数据源

在spring中有一个抽象类AbstractRoutingDataSource类,通过这个类可以实现动态数据源切换。如下是这个类的成员变量

private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private Map<Object, DataSource> resolvedDataSources;
  • targetDataSources保存了key和数据库连接的映射关系
  • defaultTargetDataSource标识默认的连接
  • resolvedDataSources这个数据结构是通过targetDataSources构建而来,存储结构也是数据库标识和数据源的映射关系

自定义DynamicDataSource,继承AbstractRoutingDataSource类

复制代码
package com.ttbank.flep.util;

import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author lucky
 * @Date 2023/9/20 9:49
 */
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
    private ConcurrentHashMap<Object,Object> targetDataSources;

    @Resource
    private Environment env;

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceHolder.getDataSource();
    }

    public synchronized void setTargetDataSources(Map<Object,Object> targetDataSources){
        ConcurrentHashMap<Object,Object> map=new ConcurrentHashMap<>();
        map.putAll(targetDataSources);
        this.targetDataSources=map;
        super.setTargetDataSources(map);
        super.afterPropertiesSet();
    }

    public synchronized void addDataSource(String dataSourceKey,String driverClassName,String url,String username,String password){
        if(dataSourceKey==null|| "".equals(dataSourceKey)){
            log.warn("dataSourceKey is empty");
        }

        DataSourceUsedHolder.use();
        Object value = this.targetDataSources.get(dataSourceKey);
        if(value==null){
            value=this.targetDataSources.get(dataSourceKey);
            if(value==null){
                DataSource dataSource = this.createDataSource(driverClassName, url, username, password);
                this.targetDataSources.put(dataSourceKey,dataSource );
                this.setTargetDataSources(this.targetDataSources);
                log.info("add DataSource named [{}]",dataSourceKey);
                return;
            }
        }
        log.info("DataSource named [{]] already exists,ignore to add",dataSourceKey);
    }

    private DataSource createDataSource(String driverClassName,String url,String username,String password){
        HikariDataSource ds=new HikariDataSource();
        try{
            ds.setJdbcUrl(url);
            ds.setUsername(username);
            ds.setPassword(password);
            ds.setDriverClassName(driverClassName);
        }catch (Exception e){
            e.printStackTrace();
        }
        return ds;

    }

}
复制代码

AbstractRoutingDataSource实现了InitializingBean接口,并实现了afterPropertiesSet方法。afterPropertiesSet方法是初始化bean的时候执行,可以针对某个具体的bean进行执行;

下面是AbstractRoutingDataSource类中afterPropertiesSet方法的关键源码;

复制代码
@Override
public void afterPropertiesSet() {
    if (this.targetDataSources == null) {
        throw new IllegalArgumentException("Property 'targetDataSources' is required");
    }
    this.resolvedDataSources = new HashMap<Object, DataSource> (this.targetDataSources.size()); //初始化resolvedDataSources 
        //循环targetDataSources,并添加到resolvedDataSources中
    for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
        Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
        DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
        this.resolvedDataSources.put(lookupKey, dataSource);
    }
        //如果默认数据源不为空则指定对应数据源
    if (this.defaultTargetDataSource != null) {
        this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
    }
}
复制代码

这里的targetDataSources属性(map)是存储将要切换的多数据源bean信息。

而如果数据源在数据库中,则需要重写方法determineCurrentLookupKey(),因为数据源bean是动态生成的,然后需要添加到targetDataSources中,此时需要调用afterPropertiesSet()方法,来通知spring有bean更新。

因为此抽象类中都是引用resolvedDataSources属性,所以在此方法中将targetDataSources属性的键值信息存储到resolvedDataSources属性中,以便后续调用。

最后,看一下AbstractRoutingDataSource类中的连接数据库的getConnection()方法,调用的是determineTargetDataSource()方法,来创建连接。

复制代码
@Override
public Connection getConnection() throws SQLException {
    return determineTargetDataSource().getConnection();
}
 
@Override
public Connection getConnection(String username, String password) throws SQLException {
    return determineTargetDataSource().getConnection(username, password);
}
复制代码

而determineTargetDataSource()方法是决定spring容器连接那个数据源

复制代码
protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    //具体选择哪个数据源
    Object lookupKey = determineCurrentLookupKey();
    DataSource dataSource = this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
        dataSource = this.resolvedDefaultDataSource;
    }
    if (dataSource == null) {
        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    }
    return dataSource;
}
复制代码

而选择哪个数据源又是由determineCurrentLookupKey()方法来决定的,此方法是抽象方法,需要我们继承AbstractRoutingDataSource抽象类来重写此方法。该方法返回一个key,该key是bean中的beanName,并赋值给lookupKey,由此key可以通过resolvedDataSources属性的键来获取对应的DataSource值,从而达到数据源切换的功能。

3.利用ThreadLocal实现数据源线程隔离

 定义DynamicDataSourceHolder 

复制代码
package com.ttbank.flep.util;

/**
 * @Author lucky
 * @Date 2023/9/20 9:43
 */
public class DynamicDataSourceHolder {
    public static final ThreadLocal<String> holder=new ThreadLocal<>();

    public DynamicDataSourceHolder() {
    }

    public static synchronized void setDataSource(String name){
        holder.set(name);
        DataSourceUsedHolder.use();
    }

    public static synchronized String getDataSource(){
        return holder.get();
    }

    public static void clearDataSource(){
        holder.remove();
    }
}
复制代码

定义DataSourceUsedHolder

复制代码
package com.ttbank.flep.util;

/**
 * @Author lucky
 * @Date 2023/9/20 9:38
 */
public class DataSourceUsedHolder {
    public static final ThreadLocal<Boolean> holder=new ThreadLocal<>();

    public DataSourceUsedHolder() {
    }

    public static void use(){
        holder.set(true);
    }

    public static Boolean IsUsed(){
        return holder.get();
    }

    public static void reSet(){
        holder.remove();
    }
}
复制代码

注意:每个接口或线程使用完数据源后,需要将数据源清空,否则会出现多线程情况下数据源切换混乱的情况;建议添加以下代码

DynamicDataSourceHolder.clearDataSource();

4 定义数据源操作接口

用于统一添加数据源;

public interface DataSourceService {
    String addDataSource(DataSourceInput dataSourceInput);
}
复制代码
package com.ttbank.flep.service;

import com.ttbank.flep.util.DataSourceInput;
import com.ttbank.flep.util.DynamicDataSource;

import javax.annotation.Resource;
import javax.sql.DataSource;

/**
 * @Author lucky
 * @Date 2023/9/20 10:27
 */
public class DataSourceServiceImpl implements DataSourceService {
    @Resource
    private DataSource dataSource;


    @Override
    public String addDataSource(DataSourceInput input) {
        ((DynamicDataSource)this.dataSource) .addDataSource(input.getDataSourceKey(),input.getDriverClassName() ,input.getUrl() ,input.getUsername() ,input.getPassword() );
        return "success";
    }
}
复制代码

5 切换数据源工具类封装

DpDataSourceProperties用于读取yml文件中的配置;

复制代码
@Component
@ConfigurationProperties(prefix = "spring.datasource.dp")
@Data
public class DpDataSourceProperties {
    private String dataSourceKey;
    private String driverClassName;
    private String url;
    private String username;
    private String password;
}
复制代码

数据源传参数DTO

复制代码
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DataSourceInput {
    private String dataSourceKey;
    private String driverClassName;
    private String url;
    private String username;
    private String password;
}
复制代码

数据源切换工具类:

复制代码
package com.ttbank.flep.util;

import com.ttbank.flep.pojo.DpDataSourceProperties;
import com.ttbank.flep.service.DataSourceService;
import org.apache.commons.lang.StringUtils;

/**
 * @Author lucky
 * @Date 2023/9/20 10:32
 */
public class DataSourceChangeUtil {
    public static DataSourceInput generateDataSource(String dataSourceKey,String driverClassName,String url,String username,String password){
        DataSourceInput input=new DataSourceInput();
        input.setDataSourceKey(dataSourceKey);
        if(StringUtils.isEmpty(dataSourceKey)){
            driverClassName="com.mysql.jdbc.Driver";
        }
        input.setDriverClassName(driverClassName);
        input.setUrl(url);
        input.setUsername(username);
        input.setPassword(password);
        return input;
    }

    public static void changeDpDataSoource(DpDataSourceProperties dpDataSourceProperties, DataSourceService dataSourceService){
        DynamicDataSourceHolder.clearDataSource();
        DataSourceInput dataSourceInput = generateDataSource(dpDataSourceProperties.getDataSourceKey(), dpDataSourceProperties.getDriverClassName(), dpDataSourceProperties.getUrl(), dpDataSourceProperties.getUsername(), dpDataSourceProperties.getPassword());
        dataSourceService.addDataSource(dataSourceInput);
        //切换数据源
        DynamicDataSourceHolder.setDataSource("dp");
    }
}
复制代码

 

参考文献:

https://blog.csdn.net/syslbjjly/article/details/93492535(推荐)

https://blog.csdn.net/qq_44665283/article/details/129237053

https://blog.csdn.net/qq_32037561/article/details/126082651(推荐)

posted @   雨后观山色  阅读(273)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
历史上的今天:
2022-09-14 RocketMQ 01---RocketMQ 介绍及基本概念
2021-09-14 Java基础知识11--Optional类
点击右上角即可分享
微信分享提示