7

一种实现Spring动态数据源切换的方法 - 京东云开发者

 1 year ago
source link: https://www.cnblogs.com/Jcloud/p/17491451.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

不在现有查询代码逻辑上做任何改动,实现dao维度的数据源切换(即表维度)

2 使用场景

节约bdp的集群资源。接入新的宽表时,通常uat验证后就会停止集群释放资源,在对应的查询服务器uat环境时需要查询的是生产库的表数据(uat库表因为bdp实时任务停止,没有数据落入),只进行服务器配置文件的改动而无需进行代码的修改变更,即可按需切换查询的数据源。

2.1 实时任务对应的集群资源

a8eaf976577f4d9785f2887f8ed81ba5~tplv-k3u1fbpfcp-zoom-1.image

2.2 实时任务产生的数据进行存储的两套环境

434e640217f94a1b82a0145036ed27be~tplv-k3u1fbpfcp-zoom-1.image

2.3 数据使用系统的两套环境(查询展示数据)

ce66739c312c4c36879c345bff3faf1d~tplv-k3u1fbpfcp-zoom-1.image

即需要在zhongyouex-bigdata-uat中查询生产库的数据。

3 实现过程

3.1 实现重点

  1. org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
    spring提供的这个类是本次实现的核心,能够让我们实现运行时多数据源的动态切换,但是数据源是需要事先配置好的,无法动态的增加数据源。
  2. Spring提供的Aop拦截执行的mapper,进行切换判断并进行切换。

注:另外还有一个就是ThreadLocal类,用于保存每个线程正在使用的数据源。

3.2 AbstractRoutingDataSource解析

public abstract class AbstractRoutingDataSource extends AbstractDataSource 
implements InitializingBean{
    @Nullable
    private Map<Object, Object> targetDataSources;

    @Nullable
    private Object defaultTargetDataSource;

    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }
    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;
    }
    @Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = resolveSpecifiedLookupKey(key);
            DataSource dataSource = resolveSpecifiedDataSource(value);
            this.resolvedDataSources.put(lookupKey, dataSource);
        });
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }

从上面源码可以看出它继承了AbstractDataSource,而AbstractDataSource是javax.sql.DataSource的实现类,拥有getConnection()方法。获取连接的getConnection()方法中,重点是determineTargetDataSource()方法,它的返回值就是你所要用的数据源dataSource的key值,有了这个key值,resolvedDataSource(这是个map,由配置文件中设置好后存入targetDataSources的,通过targetDataSources遍历存入该map)就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。

看完源码,我们可以知道,只要扩展AbstractRoutingDataSource类,并重写其中的determineCurrentLookupKey()方法返回自己想要的key值,就可以实现指定数据源的切换!

3.3 运行流程

  1. 我们自己写的Aop拦截Mapper
  2. 判断当前执行的sql所属的命名空间,然后使用命名空间作为key读取系统配置文件获取当前mapper是否需要切换数据源
  3. 线程再从全局静态的HashMap中取出当前要用的数据源
  4. 返回对应数据源的connection去做相应的数据库操作

3.4 不切换数据源时的正常配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

<!-- clickhouse数据源   -->
    <bean id="dataSourceClickhousePinpin" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" lazy-init="true">
        <property name="url" value="${clickhouse.jdbc.pinpin.url}" />
    </bean>

    <bean id="singleSessionFactoryPinpin" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- ref直接指向 数据源dataSourceClickhousePinpin  -->
<property name="dataSource" ref="dataSourceClickhousePinpin" />
    </bean>

</beans>

3.5 进行动态数据源切换时的配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- clickhouse数据源 1  -->
    <bean id="dataSourceClickhousePinpin" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" lazy-init="true">
        <property name="url" value="${clickhouse.jdbc.pinpin.url}" />
    </bean>
<!-- clickhouse数据源 2  -->
    <bean id="dataSourceClickhouseOtherPinpin" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" lazy-init="true">
        <property name="url" value="${clickhouse.jdbc.other.url}" />
    </bean>
 <!-- 新增配置 封装注册的两个数据源到multiDataSourcePinpin里 -->
 <!-- 对应的key分别是 defaultTargetDataSource和targetDataSources-->
    <bean id="multiDataSourcePinpin" class="com.zhongyouex.bigdata.common.aop.MultiDataSource">
      <!-- 默认使用的数据源-->
<property name="defaultTargetDataSource" ref="dataSourceClickhousePinpin"></property>
        <!-- 存储其他数据源,对应源码中的targetDataSources -->
<property name="targetDataSources">
            <!-- 该map即为源码中的resolvedDataSources-->
            <map>
                <!-- dataSourceClickhouseOther 即为要切换的数据源对应的key -->
<entry key="dataSourceClickhouseOther" value-ref="dataSourceClickhouseOtherPinpin"></entry>
            </map>
        </property>
    </bean>

    <bean id="singleSessionFactoryPinpin" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- ref指向封装后的数据源multiDataSourcePinpin  -->
<property name="dataSource" ref="multiDataSourcePinpin" />
    </bean>
</beans>

核心是AbstractRoutingDataSource,由spring提供,用来动态切换数据源。我们需要继承它,来进行操作。这里我们自定义的com.zhongyouex.bigdata.common.aop.MultiDataSource就是继承了AbstractRoutingDataSource

package com.zhongyouex.bigdata.common.aop;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
 * @author: cuizihua
 * @description: 动态数据源
 * @date: 2021/9/7 20:24
 * @return
 */
public class MultiDataSource extends AbstractRoutingDataSource {

    /* 存储数据源的key值,InheritableThreadLocal用来保证父子线程都能拿到值。
     */
    private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<String>();

    /**
     * 设置dataSourceKey的值
     *
     * @param dataSource
     */
    public static void setDataSourceKey(String dataSource) {
        dataSourceKey.set(dataSource);
    }

    /**
     * 清除dataSourceKey的值
     */
    public static void toDefault() {
        dataSourceKey.remove();
    }

    /**
     * 返回当前dataSourceKey的值
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return dataSourceKey.get();
    }
}

3.6 AOP代码

package com.zhongyouex.bigdata.common.aop;
import com.zhongyouex.bigdata.common.util.LoadUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;

/**
 * 方法拦截  粒度在mapper上(对应的sql所属xml)
 * @author cuizihua
 * @desc 切换数据源
 * @create 2021-09-03 16:29
 **/
@Slf4j
public class MultiDataSourceInterceptor {
//动态数据源对应的key
    private final String otherDataSource = "dataSourceClickhouseOther";

    public void beforeOpt(JoinPoint mi) {
//默认使用默认数据源
        MultiDataSource.toDefault();
        //获取执行该方法的信息
        MethodSignature signature = (MethodSignature) mi.getSignature();
        Method method = signature.getMethod();
        String namespace = method.getDeclaringClass().getName();
//本项目命名空间统一的规范为xxx.xxx.xxxMapper
        namespace = namespace.substring(namespace.lastIndexOf(".") + 1);
//这里在配置文件配置的属性为xxxMapper.ck.switch=1 or 0  1表示切换
        String isOtherDataSource = LoadUtil.loadByKey(namespace, "ck.switch");
        if ("1".equalsIgnoreCase(isOtherDataSource)) {
            MultiDataSource.setDataSourceKey(otherDataSource);
            String methodName = method.getName();
        }
    }
}

3.7 AOP代码逻辑说明

通过org.aspectj.lang.reflect.MethodSignature可以获取对应执行sql的xml空间名称,拿到sql对应的xml命名空间就可以获取配置文件中配置的属性决定该xml是否开启切换数据源了。

3.8 对应的aop配置

<!--动态数据源-->
<bean id="multiDataSourceInterceptor" class="com.zhongyouex.bigdata.common.aop.MultiDataSourceInterceptor" ></bean>
<!--将自定义拦截器注入到spring中-->
<aop:config proxy-target-class="true" expose-proxy="true">
    <aop:aspect ref="multiDataSourceInterceptor">
        <!--切入点,也就是你要监控哪些类下的方法,这里写的是DAO层的目录,表达式需要保证只扫描dao层-->
        <aop:pointcut id="multiDataSourcePointcut" expression="execution(*  com.zhongyouex.bigdata.clickhouse..*.*(..)) "/>
        <!--在该切入点使用自定义拦截器-->
        <aop:before method="beforeOpt" pointcut-ref="multiDataSourcePointcut" />
    </aop:aspect>
</aop:config>

以上就是整个实现过程,希望能帮上有需要的小伙伴

作者:京东物流 崔子华

来源:京东云开发者社区


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK