关于spring:一种实现Spring动态数据源切换的方法-京东云技术团队

82次阅读

共计 7404 个字符,预计需要花费 19 分钟才能阅读完成。

1 指标

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

2 应用场景

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

2.1 实时工作对应的集群资源

[]()

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

[]()

2.3 数据应用零碎的两套环境(查问展现数据)

[]()

即须要在 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>

以上就是整个实现过程,心愿能帮上有须要的小伙伴

作者:京东物流 崔子华

起源:京东云开发者社区

正文完
 0