写点什么

一种实现 Spring 动态数据源切换的方法 | 京东云技术团队

  • 2023-06-19
    北京
  • 本文字数:4821 字

    阅读完需:约 16 分钟

一种实现Spring动态数据源切换的方法 | 京东云技术团队

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

  2. spring 提供的这个类是本次实现的核心,能够让我们实现运行时多数据源的动态切换,但是数据源是需要事先配置好的,无法动态的增加数据源。

  3. 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 **/@Slf4jpublic 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>
复制代码


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


作者:京东物流 崔子华

来源:京东云开发者社区

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
一种实现Spring动态数据源切换的方法 | 京东云技术团队_spring_京东科技开发者_InfoQ写作社区