首页 > 其他分享 >深度理解 Spring 动态数据源切换是如何实现的

深度理解 Spring 动态数据源切换是如何实现的

时间:2024-01-20 15:32:34浏览次数:22  
标签:数据源 切面 DataSource Spring TargetDataSource public 切换


更新(不是必读,只为了帮助读者更好的理解执行过程)

  1. 2022-11-16 结合事务 TransactionInterceptor 的执行,剖析数据源是如何切换的
  2. 详细分析为什么,切面要设置 @Order(-9999) 属性

针对点一回答如下

在Spring Boot 项目启动的时候,会去扫描所有配置类,生成一个个的 Bean ,被 @Transaction 标记的方法其对应的类,会被 Aop 增强生成一个增强 Bean,当我们调用增强 Bean 中的方法前,会执行我们的增强逻辑,这些增强器(图一)逻辑就是执行一个个的 Interceptor 等

图一

深度理解 Spring 动态数据源切换是如何实现的_spring


在执行 TransactionInterceptor 的时候(涉及到 Spring 事务源码),会为我们开启一个连接(图二)并设置 Commit =false图二

深度理解 Spring 动态数据源切换是如何实现的_spring_02

其中在获取连接的时候 Spring 为我们提供了一个扩展类(图三),只要我们实现了这个类,对应的 Jdbc 操作就会解析我们自己注入的数据源,完成对数据库的 JDBC 操作,到此切换数据源的完整流程结束

图三

深度理解 Spring 动态数据源切换是如何实现的_数据源_03

针对点二回答如下

我们编写的切面执行流程也是,走上图一中的流程,只不过是执行 MethodBeforeAdviceInterceptor 这个增强器(图四),如果不设置 @Order(-9999) 那么 TransactionInterceptor 会先于 MethodBeforeAdviceInterceptor 执行,就会造成一个结果就是,这边都开始开启一个事务,开始获取连接了,由于数据源切换切面此时还未执行,所以此时钩子方法这里不知道该用哪一个数据源去获取连接,就会报错(图五)

图四

深度理解 Spring 动态数据源切换是如何实现的_动态数据源切换_04


图五

深度理解 Spring 动态数据源切换是如何实现的_aop执行流程_05


上述俩个更新点涉及到的源码知识:

aop增强器执行流程,手把手带你debug (文章末尾有干货)

全面解读spring注解事务失效场景,伪代码+图文深度解析spring源码运行过程

前言

小憩是辣么的让人神往,就像备战高考靠窗位置的那个你,肆无忌道的放空自己,望着深蓝色宁静的天空,思考着未来该何去何从,近处一颗高大魁梧的银杏树在炎炎夏日中尽情的摇曳着自己嫩绿的枝丫,迸发出无尽的希望,回想起来一切都是这么的美好。好了今日的杂想到此结束,回归正文,关于动态数据源切换那点事。

引导思考

如果咱们现在生活在互联网刚开始兴起的那个时期,万物堵塞,所有和数据库打交道的操作只能通过 JDBC 来实现,恰巧你是一个技术狂热爱好者,想通过自己的努力封装一套半 ORM 框架,造福千千万程序员,你会如何实现动态数据源切换这个扩展点呢?

  1. 当用户没有动态数据源切换的需求时:框架加载默认数据源给用户使用
  2. 当用户有动态数据源切换的需求时:提供一个官方认证的工具给用户使用,用户只需按照要求将多数据源配置好,系统会加载用户自定义的数据源

对于点一很好办:我们在框架内部默认创建一个名字为 dataSource(Spring Boot 默认数据源:HikariDataSource)、类型为 javax.sql.DataSource 的这么一个 数据源就好了。
对于点二来说:这个工具应该让用户的学习使用成本越低越好。从用户角度上分析,应该没有人希望把多数据源配置配在 Excel、Txt文件中吧,最优解配在 yml 文件中,然后利用 Spring 中的 Environment 类可以很容易加载到多数据源的配置。(题外话:基于 Spring 生态开发,少走弯路 30 年),到底最后是选择使用哪个数据源,设计思路有俩种:

  1. 可以支持加载多个数据源到系统中,写一个抽象类 AbstractDataSourceRoute 里面实现了 DataSource choiceDataSource(String type),将到底是使用哪个数据源的方法交给子类去实现 abstract String type(),这样用户只需实现 AbstractDataSourceRoute 类,指定一下使用哪个数据源,就可以了。伪代码如下
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.sql.DataSource;
import java.util.Map;

public abstract class DataSourceUtil implements DataSource, ApplicationContextAware, DisposableBean {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    DataSource choiceDataSource() {
        //1:通过 applicationContext 获取所有数据源
        Map<String, DataSource> dataSourceMap = applicationContext.getBeansOfType(DataSource.class);
        //2:通过 type 筛选指定要用的数据源
        DataSource dataSource = dataSourceMap.get(type());
        //3:返回用户指定要使用的数据源
        return dataSource;
    }
    abstract String type();
}

2: 限制系统加载一个数据源,扫描到自定义数据源,就不加载默认数据源,反之加载默认数据源

动态数据源切换之应用

先来简单介绍一下啊动态数据源用到的开发场景:

  1. 项目需要与其他系统对接,库是别人,因此需要配置多套数据源
  2. 项目本身根据实际需求设计数据库的时候,用到了多种的数据库,例如 Mysql、Oracle同时使用,当然数据互通是个问题

使用场景介绍完了,接下来简单介绍一下如何使用吧~,就是读取 yml 配置中我们配置的数据源属性,利用@ConfigurationProperties注解完成属性的自动填充,继而注入到 IOC 容器中。这时候系统读取到了多个数据源,但是还不清楚什么时候用哪个数据源呢,因此我们可以编写一个切面,来动态的告知系统该如何选择数据源

@Configuration
public class DataSourceConfig {
    @Bean(name = "master")
    @ConfigurationProperties(prefix = "spring.datasource.druid.master.datasource")
    public DataSource master() {
        return new DruidDataSource();
    }

    @Bean(name = "slave")
    @ConfigurationProperties(prefix = "spring.datasource.druid.salve.datasource")
    public DataSource slave() {
        return new DruidDataSource();
    }

    @Bean
    @Primary
    public DynamicDataSource multipleDataSource(@Qualifier("master") DataSource db1, @Qualifier("slave") DataSource db2) {
        DynamicDataSource multipleDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.getMASTER(), db1);
        targetDataSources.put(DataSourceType.getSALVE(), db2);
        multipleDataSource.setTargetDataSources(targetDataSources);
        multipleDataSource.setDefaultTargetDataSource(db1);
        DynamicDataSourceContextHolder.dataSourceIds.add(DataSourceType.getSALVE());
        DynamicDataSourceContextHolder.dataSourceIds.add(DataSourceType.getMASTER());
        return multipleDataSource;
    }

    @Data
    static class DataSourceType {

        private static String MASTER = "master";
        private static String SALVE = "salve";

        public static String getMASTER() {
            return MASTER;
        }

        public static String getSALVE() {
            return SALVE;
        }
    }
}

Aspect 切面

编写自定义注解 TargetDataSource ,并通过切面拦截它,根据 TargetDataSource 中的属性做判断,选择特定的数据源。里面的逻辑没啥好看的,就是在方法执行之前,获取方法或者类上面 TargetDataSource 中指定的属性,填充到 DynamicDataSourceContextHolder 中,后续我们重写 Spring 为我们提供的数据源选择钩子方法,返回 DynamicDataSourceContextHolder 中的数据就好了。这样一来就实现了,数据源切换的需求了,接下来看下这个钩子方法里面的源码吧~~

@Slf4j
@Aspect
@Order(-10)
@Component
public class AspectWithinAnnotation {

    /**
     * @within:拦截类上的注解
     * @annotation:拦截方法上的注解
     */
    @Before("@within(com.example.oraceldemo.config.aspect.TargetDataSource)||@annotation(com.example.oraceldemo.config.aspect.TargetDataSource)")
    public void changeDataSource(JoinPoint joinPoint) {
        TargetDataSource targetDataSource = getTargetDataSource(joinPoint);
        if (targetDataSource == null || !DynamicDataSourceContextHolder.isContainsDataSource(targetDataSource.name())) {
            log.error("使用默认的数据源 -> " + joinPoint.getSignature());
        } else {
            log.debug("使用数据源:" + targetDataSource.name());
            DynamicDataSourceContextHolder.setDataSourceType(targetDataSource.name());
        }
    }

    @After("@within(com.example.oraceldemo.config.aspect.TargetDataSource)||@annotation(com.example.oraceldemo.config.aspect.TargetDataSource)")
    public void clearDataSource(JoinPoint joinPoint) {
        log.debug("清除数据源 " + getTargetDataSource(joinPoint).name() + " !");
        DynamicDataSourceContextHolder.clearDataSourceType();
    }

    /**
     * 先从方法上获取 TargetDataSource 注解,获取不到从类上面获取
     */
    public TargetDataSource getTargetDataSource(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        TargetDataSource targetDataSource = method.getAnnotation(TargetDataSource.class);
        if (targetDataSource == null) {
            Class<?> declaringClass = method.getDeclaringClass();
            targetDataSource = declaringClass.getAnnotation(TargetDataSource.class);
        }
        return targetDataSource;
    }
}

Spring 扩展点之 AbstractRoutingDataSource

又是这个 Abstract 开头的类~,记住所有以 Abstract 开头的类,他的父类才是真正干活的人,因为详细的逻辑都封装在父类中了,爸爸才是全家的顶梁柱、主心骨啊,所有的风雨、压力都是爸爸来抗,只为了子女脸上洋溢着的灿烂的笑容。

public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 选择数据源钩子方法
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    public DynamicDataSource() {
    }
}

浅浅的 debug 一下源码吧

在钩子方法中打一个断点,看一下它的执行链路,一次往上翻就好了,来到 AbstractRoutingDataSource 中的 determineTargetDataSource 方法,粗略的一看,这代码和本文开头引导思考中的伪代码,不就是一个模子里刻出来的咩~由于代码思路都是一样的,就不过多的做分析了。为了读者方便理解再次贴一遍原话如下~

写一个抽象类 AbstractDataSourceRoute 里面实现了 DataSource choiceDataSource(String type),将到底是使用哪个数据源的方法交给子类去实现 abstract String type(),这样用户只需实现 AbstractDataSourceRoute 类中的钩子方法,指定一下使用哪个数据源,就可以了。

深度理解 Spring 动态数据源切换是如何实现的_aop执行流程_06

浅浅的解读一下 resolvedDataSources 吧

上图一从 resolvedDataSources 中根据钩子方法的返回值,获取指定的数据源返回,那么 resolvedDataSources 中的数据源是什么时候注入的呢?答案入下图,利用了 Spring 中的 InitializingBean 接口,在Bean属性填充完毕后,将 targetDataSources 中数据源全部放到 resolvedDataSources 中,这样一来,我们用户只需指定指定钩子方法中的数据源类别,当方法被调用的时候,切面就会截取方法、类上面的自定义注解,填充到 ThreadLocal 中,然后后续的 Mybatis 获取数据源查 DB 的时候,根据钩子方法的返回值,从 resolvedDataSources 中获取指定的数据源然后查 DB 从而实现了,数据源随意切换的效果

深度理解 Spring 动态数据源切换是如何实现的_aop执行流程_07

注意点

由于本文中的切面拦截的是自定义注解,且切入点是使用 @within、@annotation来进行修饰的(即只会识别被调用方法对应的类上、或者是被调用方法上是否存在自定义注解),考虑到现在大多数人都在用 MybatisPlus~,正确用法入下图一、二,图三为错误用法,因为,我们调用的 List 方法本质还是 ServiceImpl 类上的,然而ServiceImpl 类上没有有被我们的注解修饰,故此时切面会失效,本文切面解析注解的范围如下图四。当然可能还有人切入点采用 @Before(“execution(* com.example.oraceldemo.service..(…))”) ,也可能出现切面失效、切换数据源失效的情况,大致的分析过程差不多,读者有兴趣可以自行去分析哦~~~

深度理解 Spring 动态数据源切换是如何实现的_aop执行流程_08


深度理解 Spring 动态数据源切换是如何实现的_数据源_09


深度理解 Spring 动态数据源切换是如何实现的_事务源码_10


深度理解 Spring 动态数据源切换是如何实现的_事务源码_11

@Autowired
private TabStarService tabStarService;

@Test
void xiaomi() {
    List<Goods> list = goodsService.list();
    System.err.println(list);
}

小咸鱼的技术窝

关注不迷路,日后分享更多技术干货,B站、微信公众号同名,名称都是(小咸鱼的技术窝)更多详情在主页


标签:数据源,切面,DataSource,Spring,TargetDataSource,public,切换
From: https://blog.51cto.com/u_16414043/9345743

相关文章

  • Spring IOC 容器加载过程详解
    在Spring框架中,IOC(InversionofControl)容器是核心的概念之一。IOC容器负责管理和装配各个组件,本文将详细介绍SpringIOC容器的加载过程,包括如何配置、初始化和装配Bean。1.什么是IOC容器IOC容器是Spring框架的一个关键组件,负责管理Java对象的生命周期、配置信息以及对象之间的......
  • SpringBoot项目通过注解快速解决,字典翻译,响应数据加密,数据脱敏等问题
    简介在几乎所有SpringBoot项目中都会面临字典翻译,接口数据加密,数据脱敏的问题。在每个接口中单独的解决会非常繁琐,因此接下来介绍一下怎么通过注解快速解决这些问题。实现步骤1.引入maven坐标<dependency><groupId>io.gitee.gltqe</groupId><artifactId>......
  • Spring AOP原来是这样实现的
    SpringAOP技术实现原理在Spring框架中,AOP(面向切面编程)是通过代理模式和反射机制来实现的。本文将详细介绍SpringAOP的技术实现原理,包括JDK动态代理和CGLIB代理的使用,并通过实例演示其在实际项目中的应用。1.AOP的实现原理概述SpringAOP的实现基于代理模式,通过代理对象来包......
  • Java21 + SpringBoot3集成WebSocket
    目录前言相关技术简介什么是WebSocketWebSocket的原理WebSocket与HTTP协议的关系WebSocket优点WebSocket应用场景实现方式添加maven依赖添加WebSocket配置类,定义ServerEndpointExporterBean定义WebSocketEndpoint前端创建WebSocket对象总结前言近日心血来潮想做一个开源项目,目......
  • 多数据源事务——@DSTransactional注解原理
    1.前言在前面的文章中,提到一种手动提交多数据源事务的实现方式,dynamic-datasource包为我们提供了一种更为优雅,开箱即用的注解,即@DSTransactional,因为spring提供的@Tansactional注解是不支持多数据源的,@DSTransactional注解的出现刚好可以很好的弥补这一点。@DS注解和@DSTransacti......
  • spring--Bean的生命周期
    Springbean的生命周期涉及多个阶段,从创建到销毁。下面是一个简化的生命周期描述:Bean定义:首先,Spring根据配置(XML、注解或Java配置)创建一个bean的定义。Bean实例化:Spring容器使用构造器或工厂方法实例化bean。属性填充:Spring容器通过反射机制,将所有......
  • spring--是如何解决单例模式下循环依赖问题的
    Spring解决单例bean的循环依赖主要依赖于容器的三级缓存机制,以及bean的提前暴露。这里是它如何工作的:三级缓存:一级缓存(singletonObjects):存储已经经过完整生命周期处理的单例bean,包括初始化和依赖注入等。二级缓存(earlySingletonObjects):存储早期的单例对象的引用,这些......
  • spring--@Autowired @Qualifier @Resource @Value 四者的区别
    @Autowired,@Qualifier,@Resource,和@Value是Spring框架中用于依赖注入的注解,它们各有特点和用途:@Autowired:@Autowired注解用于自动装配Spring容器中的bean。它默认按类型(byType)进行依赖注入。当存在多个同类型的bean时,它可以和@Qualifier注解一起使用,以指定注入......
  • 冷泉港实验室 (The Cold Spring Harbor Laboratory)
    冷泉港实验室(TheColdSpringHarborLaboratory)又译为科尔德斯普林实验室,是一个非盈利的私人科学研究与教育中心,位于美国纽约州长岛上的冷泉港,此机构的研究对象包括癌症、神经生物学、植物遗传学、基因组学以及生物资讯学,其主要成就为分子生物学领域,在该研究所一共诞生了7......
  • 【Mybatis-Plus】Mybatis-Plus多数据源(三)
    参考官网:多数据源|MyBatis-Plus(baomidou.com)使用方法1、引入dynamic-datasource-spring-boot-starter。1<dependency>2<groupId>com.baomidou</groupId>3<artifactId>dynamic-datasource-spring-boot-starter</artifactId>4&l......