Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘(下)

在上一篇博客《Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘》介绍了dynamic-datasource-spring-boot-starter的自动配置类和配置属性类之后,本文继续来剖析多数据源是如何切换的,什么时候切换的。

前文中提到dynamic-datasource-spring-boot-starter的自动配置类DynamicDataSourceAutoConfiguration在初始化的时候,会创建一个Advisor bean。

	@Role(2)
    @Bean
    public Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
        DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(this.properties.isAllowedPublicOnly(), dsProcessor);
        DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
        advisor.setOrder(this.properties.getOrder());
        return advisor;
    }

该bean负责处理带有@DS注解的类或方法。它发挥的作用和通过@Pointcut+@Before或@Around发挥的作用一致。更多细节可见《探索微服务中的权限控制:一次线上问题排查的思考》

public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {

    private final Advice advice;

    private final Pointcut pointcut;

    public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) {
        // 初始化的时候,传入了dynamicDataSourceAnnotationInterceptor
        // 说明拦截器发挥了通知的作用。
        this.advice = dynamicDataSourceAnnotationInterceptor;
        this.pointcut = buildPointcut();
    }

	// 核心方法, 切点获取,类比@Pointcut注解 
    @Override
    public Pointcut getPointcut() {
        return this.pointcut;
    }

    // 核心方法,通知获取, 类比@Before, @After, @Around注解
    @Override
    public Advice getAdvice() {
        return this.advice;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        if (this.advice instanceof BeanFactoryAware) {
            ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
        }
    }

    private Pointcut buildPointcut() {
        Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);
        Pointcut mpc = new AnnotationMethodPoint(DS.class);
        return new ComposablePointcut(cpc).union(mpc);
    }
    ……
}

DynamicDataSourceAnnotationAdvisor类在初始化的时候,传入了DynamicDataSourceAnnotationInterceptor类型的拦截器作为advice, 说明该拦截器发挥了通知的作用(类似@Before, @Around)。

public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {

    /**
     * The identification of SPEL.
     */
    private static final String DYNAMIC_PREFIX = "#";

    private final DataSourceClassResolver dataSourceClassResolver;
    private final DsProcessor dsProcessor;

    public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {
        dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly);
        this.dsProcessor = dsProcessor;
    }

	// 核心方法, dsKey就是@DS注解中的值, 如@DS("cusOB"),那么dsKey = "cusOB"
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        String dsKey = determineDatasourceKey(invocation);
        DynamicDataSourceContextHolder.push(dsKey);
        try {
            return invocation.proceed();
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
    private String determineDatasourceKey(MethodInvocation invocation) {
        String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
        return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
    }
}

在invoke方法执行前,会先把该数据源对应的key加入到线程本地变量的双端队列中,如下。

private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
        @Override
        protected Deque<String> initialValue() {
            return new ArrayDeque<>();
        }
    };

在invoke方法执行后,会先把该数据源对应的key从线程本地变量的双端队列中移除。

看到这里,我们知道,dynamic-datasource-spring-boot-starter会通过DynamicDataSourceAnnotationInterceptor 拦截器获取到@DS注解标识的方法或者类的代理。而@DS注解标注的就是数据源的Key。有了该key, Mybatis就可以获取到该key对应的Druid数据库连接池了。具体的策略,dynamic-datasource-spring-boot-starter提供了轮询和随机两种,这里就不再赘述了。

数据源的切换逻辑在DynamicRoutingDataSource类中。它继承了AbstractRoutingDataSource类,该类实现了getConnection()方法。

public abstract class AbstractRoutingDataSource extends AbstractDataSource {

    protected abstract DataSource determineDataSource();

    @Override
    public Connection getConnection() throws SQLException {
        String xid = TransactionContext.getXID();
        if (StringUtils.isEmpty(xid)) {
        	// 核心方法determineDataSource
            return determineDataSource().getConnection();
        } else {
            String ds = DynamicDataSourceContextHolder.peek();
            ConnectionProxy connection = ConnectionFactory.getConnection(ds);
            return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
        }
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        String xid = TransactionContext.getXID();
        if (StringUtils.isEmpty(xid)) {
            return determineDataSource().getConnection(username, password);
        } else {
            String ds = DynamicDataSourceContextHolder.peek();
            ConnectionProxy connection = ConnectionFactory.getConnection(ds);
            return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection(username, password))
                    : connection;
        }
    }
}

DynamicDataSourceAnnotationInterceptor的invoke方法负责放数据源的key,
determineDataSource方法负责从线程本地变量的双端队列中取出数据源的key。

public DataSource determineDataSource() {
        String dsKey = DynamicDataSourceContextHolder.peek();
        return getDataSource(dsKey);
    }

getDataSource方法用来获取具体的数据库连接。

    private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();


	public DataSource getDataSource(String ds) {
        if (StringUtils.isEmpty(ds)) {
            return determinePrimaryDataSource();
        } else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            return groupDataSources.get(ds).determineDataSource();
        } else if (dataSourceMap.containsKey(ds)) {
            log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
            // map中存了所有的数据库连接信息
            return dataSourceMap.get(ds);
        }
        if (strict) {
            throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
        }
        return determinePrimaryDataSource();
    }

那么问题来了,dataSourceMap的数据库相关信息是什么时候存进去的呢?

大家还记得上一篇博客中,我们提到dynamic-datasource-spring-boot-starter的自动配置类DynamicDataSourceAutoConfiguration在初始化的时候还创建了一个DynamicDataSourceProvider类型的bean嘛?没错,dataSourceMap是它从DynamicDataSourceProperties配置类中获取,然后存储进去的key是数据源的唯一标识,如value是数据库的相关信息,包括url, username, password等

	@Bean
    @ConditionalOnMissingBean 
    // 确保Spring容器中不存在 DynamicDataSourceProvider 类型的 Bean 时才创建该 Bean
    public DynamicDataSourceProvider dynamicDataSourceProvider() {
        Map<String, DataSourceProperty> datasourceMap = this.properties.getDatasource();
        return new YmlDynamicDataSourceProvider(datasourceMap);
    }

	public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider {

    /**
     * 所有数据源
     */
    private final Map<String, DataSourceProperty> dataSourcePropertiesMap;

    @Override
    public Map<String, DataSource> loadDataSources() {
    	// 核心方法
        return createDataSourceMap(dataSourcePropertiesMap);
    }
}

protected Map<String, DataSource> createDataSourceMap(Map<String, DataSourceProperty> dataSourcePropertiesMap) {
		// dataSourcePropertiesMap 就是从DynamicDataSourceProperties配置类中拿到的所有数据库信息
        Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
        for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
            DataSourceProperty dataSourceProperty = item.getValue();
            String poolName = dataSourceProperty.getPoolName();
            if (poolName == null || "".equals(poolName)) {
                poolName = item.getKey();
            }
            dataSourceProperty.setPoolName(poolName);
            dataSourceMap.put(poolName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
        }
        return dataSourceMap;
    }

最后,我们来总结下,dynamic-datasource-spring-boot-starter本质还是通过AOP实现了多数据源的切换,实现思路和编码规范、风格等都值得大家深入研究和学习。先研究透一个,其它的SpringBoot的常用starter就会手到擒来。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/942942.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【数据可视化复习方向】

1.数据可视化就是数据中信息的可视化 2.数据可视化主要从数据中寻找三个方面的信息&#xff1a;模式、关系和异常 3.大数据可视化分类&#xff1a;科学可视化、信息可视化、可视分析学 4.大数据可视化作用&#xff1a;记录信息、分析推理、信息传播与协同 5.可视化流程&…

Python 多进程编程详解

目录 一、多进程编程简介 1. 什么是多进程 2. 多进程与多线程的区别 二、Python 中的多进程编程 1. 创建进程 2. 进程间通信 3. 进程池 4. 进程同步 5. 注意事项 三、实际应用案例 四、总结 在 Python 中&#xff0c;多进程编程是一种提高程序运行效率的有效手段。相…

Redis篇--应用篇1--会话存储(session共享)

1、概述 实现Session共享是构建分布式Web应用时的一个重要需求&#xff0c;尤其是在水平扩展和高可用性要求较高的场景下。 在分布式服务或集群服务中往往会出现这样一个问题&#xff1a;用户登录A服务后可以正常访问A服务中的接口。但是我们知道&#xff0c;分布式服务通常都…

ip-协议

文章目录 1. 网络层2. ip协议2.1 ip协议格式2.2 网段划分基本概念网段划分的两种方式为什么要网段划分&#xff1f;特殊的IP地址IP地址数量不足 2.3 私有IP与公网IP2.4 路由 3. IP的分片与组装为什么要分片与组装&#xff1f;如何分片&#xff1f;如何组装&#xff1f; 1. 网络…

ECharts散点图-气泡图,附视频讲解与代码下载

引言&#xff1a; ECharts散点图是一种常见的数据可视化图表类型&#xff0c;它通过在二维坐标系或其它坐标系中绘制散乱的点来展示数据之间的关系。本文将详细介绍如何使用ECharts库实现一个散点图&#xff0c;包括图表效果预览、视频讲解及代码下载&#xff0c;让你轻松掌握…

Jmeter录制https请求

jmeter 5.5版本&#xff0c;chrome浏览器 1、首先添加Test Plan-Thread Group-HTTP(S) Test Script Recorder 2、设置HTTP(S) Test Script Recorder界面的Port&#xff08;监听端口&#xff0c;设置浏览器代理时需要与这里保持一致&#xff09;、HTPS Domains&#xff08;录制…

【Git 常用操作:pull push】

Git 基本概念 Git 是一个先进的开源的分布式版本控制系统&#xff0c;常用于管理工作内容、项目代码等功能。 Git 工作流程 图片来源&#xff1a;https://www.runoob.com/git/git-basic-operations.html 说明&#xff1a; workspace&#xff1a;工作区staging area&#xff…

LLaMA-Factory GLM4-9B-CHAT LoRA 指令微调实战

&#x1f929;LLaMA-Factory GLM LoRA 微调 安装llama-factory包 git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git进入下载好的llama-factory&#xff0c;安装依赖包 cd LLaMA-Factory pip install -e ".[torch,metrics]" #上面这步操作会完成…

基于kraft部署kafka集群

kafka介绍 Apache Kafka 是一个开源的分布式事件流平台&#xff0c;被数千家公司用于高性能数据管道、流分析、数据集成和关键任务应用。 Kafka是一个拥有高吞吐、可持久化、可水平扩展&#xff0c;支持流式数据处理等多种特性的分布式消息流处理中间件&#xff0c;采用分布式…

Day13 苍穹外卖项目 工作台功能实现、Apache POI、导出数据到Excel表格

目录 1.工作台 1.1 需求分析和设计 1.1.1 产品原型 1.1.2 接口设计 1.2 代码导入 1.2.1 Controller层 1.2.2 Service层接口 1.2.3 Service层实现类 1.2.4 Mapper层 1.3 功能测试 1.4 代码提交 2.Apache POI 2.1 介绍 2.2 入门案例 2.2.1 将数据写入Excel文件 2.2.2 读取Excel文…

Web前端基础知识(三)

表单的应用非常丰富&#xff0c;可以说&#xff0c;每个网站都会用到表单。下面首先介绍表单中的form标签。 --------------------------------------------------------------------------------------------------------------------------------- <form></form&g…

NLP中的神经网络基础

一&#xff1a;多层感知器模型 1&#xff1a;感知器 解释一下&#xff0c;为什么写成 wxb>0 &#xff0c;其实原本是 wx > t ,t就是阈值&#xff0c;超过这个阈值fx就为1&#xff0c;现在把t放在左边。 在感知器里面涉及到两个问题&#xff1a; 第一个&#xff0c;特征提…

docker安装MySQL--宝塔面板操作版

记录 1 在centos中安装宝塔面板 参照宝塔面板官方网页上步骤进行操作&#xff0c;然后登录网页地址 成功后直接拉取 成功后可以在本地镜像中看到 2 创建配置文件 cd /home/mysql/conf vim my.cnf [rootplmomn-gw conf]# cat /home/mysql/conf/my.cnf [client] #设置客户端…

C++简明教程(3)(初识VS)

一、编程工具大揭秘——IDE 当我们准备踏入 C 编程的奇妙世界时&#xff0c;首先要认识一个重要的“魔法盒子”——集成开发环境&#xff08;IDE&#xff09;。IDE 就像是一个全能的编程工作室&#xff0c;它把我们写代码所需要的各种工具都整合到了一起&#xff0c;让编程这件…

电脑出现 0x0000007f 蓝屏问题怎么办,参考以下方法尝试解决

电脑蓝屏是让许多用户头疼的问题&#xff0c;其中出现 “0x0000007f” 错误代码更是较为常见且棘手。了解其背后成因并掌握修复方法&#xff0c;能帮我们快速恢复电脑正常运行。 一、可能的硬件原因 内存问题 内存条长时间使用可能出现物理损坏&#xff0c;如金手指氧化、芯片…

分布式调度框架学习笔记

一、分布式调度框架的基本设计 二、线程池线程数量设置的基本逻辑 cpu是分时复用的方法&#xff0c;线程是cpu调度的最小单元 如果当前cpu核数是n&#xff0c;计算密集型线程数一般设为n&#xff0c;io密集型(包括磁盘io和网络io)线程数一般设置为2n. 计算密集型线程数一般设…

快速排序算法 -- 深入研究

一 . 快排性能的关键点分析 快排性能的关键点分析 : 决定快排性能的关键点是每次单趟排序后 &#xff0c; key 对数组的分割 &#xff0c; 如果每次选key 基本二分居中&#xff0c;那么快排的递归树就是颗均匀的满二叉树&#xff0c;性能最佳。但是实际中虽然不可能每次都是二…

ORA-65198 PDB clone 时 不能新加datafile 以及hang的一个原因

create pluggable database XX from SS keystore identified by "YYY" parallel 32 service_name_convert( _srv, _srv); 20TB 4小时 update /* rule */ undo$ set name:2,file#:3,block#:4,status$:5,user#:6,undosqn:7,xactsqn:8,scnbas:9,scnwrp:10,inst#:11,…

Android--java实现手机亮度控制

文章目录 1、开发需求2、运行环境3、主要文件4、布局文件信息5、手机界面控制代码6、debug 1、开发需求 需求&#xff1a;开发一个Android apk实现手机亮度控制 2、运行环境 Android studio最新版本 3、主要文件 app\src\main\AndroidManifest.xml app\src\main\res\layou…

HarmonyOS NEXT 实战之元服务:静态案例效果--- 日出日落

背景&#xff1a; 前几篇学习了元服务&#xff0c;后面几期就让我们开发简单的元服务吧&#xff0c;里面丰富的内容大家自己加&#xff0c;本期案例 仅供参考 先上本期效果图 &#xff0c;里面图片自行替换 效果图1完整代码案例如下&#xff1a; import { authentication } …