spring动态数据源,多数据源

news2024/11/18 5:40:51

Spring是如何支持多数据源的

Spring提供了一个AbstractRoutingDataSource类,用来实现对多个DataSource的按需路由,本文介绍的就是基于此方式实现的多数据源实践。

一、什么是AbstractRoutingDataSource

先看类上的注释:

Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
calls to one of various target DataSources based on a lookup key. The latter is usually
(but not necessarily) determined through some thread-bound transaction context.

课代表翻译:这是一个抽象类,可以通过一个lookup key,把对getConnection()方法的调用,路由到目标DataSource。后者(指lookup key)通常是由和线程绑定的上下文决定的。

这段注释可谓字字珠玑,没有一句废话。下文结合主要代码解释其含义。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    //目标 DataSource Map,可以装很多个 DataSource
    @Nullable
    private Map<Object, Object> targetDataSources;
    
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;

    //Bean初始化时,将 targetDataSources 遍历并解析后放入 resolvedDataSources
    @Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = CollectionUtils.newHashMap(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);
        }
    }
    
    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    /**
     * Retrieve the current target DataSource. Determines the
     * {@link #determineCurrentLookupKey() current lookup key}, performs
     * a lookup in the {@link #setTargetDataSources targetDataSources} map,
     * falls back to the specified
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
     * @see #determineCurrentLookupKey()
     */
     //根据 #determineCurrentLookupKey()返回的lookup key 去解析好的数据源 Map 里取相应的数据源
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        // 当前 lookupKey 的值由用户自己实现↓
        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;
    }
    
    /**
     * Determine the current lookup key. This will typically be
     * implemented to check a thread-bound transaction context.
     * <p>Allows for arbitrary keys. The returned key needs
     * to match the stored lookup key type, as resolved by the
     * {@link #resolveSpecifiedLookupKey} method.
     */
    // 该方法用来决定lookup key,通常用线程绑定的上下文来实现
    @Nullable
    protected abstract Object determineCurrentLookupKey();
    
    // 省略其余代码...

}

首先看类图

是个DataSource,并且实现了InitializingBean,说明有Bean的初始化操作。

其次看实例变量

private Map<Object, Object> targetDataSources;private Map<Object, DataSource> resolvedDataSources;其实是一回事,后者是经过对前者的解析得来的,本质就是用来存储多个 DataSource实例的 Map

最后看核心方法

使用DataSource,本质就是调用其getConnection()方法获得连接,从而进行数据库操作。

AbstractRoutingDataSource#getConnection()方法首先调用determineTargetDataSource(),决定使用哪个目标数据源,并使用该数据源的getConnection()连接数据库:

@Override
public Connection getConnection() throws SQLException {
   return determineTargetDataSource().getConnection();
}
protected DataSource determineTargetDataSource() {
   Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
   // 这里使用的 lookupKey 就能决定返回的数据源是哪个
   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;
}

所以重点就是determineCurrentLookupKey()方法,该方法是抽象方法,由用户自己实现,通过改变其返回值,控制返回不同的数据源。用表格表示如下:

lookupKeyDataSource
firstfirstDataSource
secondsecondDataSource

如何实现这个方法呢?结合Spring在注释里给的提示:

后者(指 lookup key)通常是由和线程绑定的上下文决定的。

应该能联想到ThreadLocal了吧!ThreadLocal可以维护一个与当前线程绑定的变量,充当这个线程的上下文。

二、实现

设计yaml文件外部化配置多个数据源

spring:
  datasource:
    first:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:mem:db1
      username: sa
      password:
    second:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:mem:db2
      username: sa
      password:

创建lookupKey的上下文持有类:

/**
 * 数据源 key 上下文
 * 通过控制 ThreadLocal变量 LOOKUP_KEY_HOLDER 的值用于控制数据源切换
 * @see RoutingDataSource
 * @author :Java课代表
 */
public class RoutingDataSourceContext {

    private static final ThreadLocal<String> LOOKUP_KEY_HOLDER = new ThreadLocal<>();

    public static void setRoutingKey(String routingKey) {
        LOOKUP_KEY_HOLDER.set(routingKey);
    }

    public static String getRoutingKey() {
        String key = LOOKUP_KEY_HOLDER.get();
        // 默认返回 key 为 first 的数据源
        return key == null ? "first" : key;
    }

    public static void reset() {
        LOOKUP_KEY_HOLDER.remove();
    }
}

实现AbstractRoutingDataSource

/**
 * 支持动态切换的数据源
 * 通过重写 determineCurrentLookupKey 实现数据源切换
 * @author :Java课代表
 */
public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return RoutingDataSourceContext.getRoutingKey();
    }

}

给我们的RoutingDataSource初始化上多个数据源:

/**
 * 数据源配置
 * 把多个数据源,装配到一个 RoutingDataSource 里
 * @author :Java课代表
 */
@Configuration
public class RoutingDataSourcesConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.first")
    public DataSource firstDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.second")
    public DataSource secondDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean
    public RoutingDataSource routingDataSource() {
        RoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setDefaultTargetDataSource(firstDataSource());
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("first", firstDataSource());
        dataSourceMap.put("second", secondDataSource());
        routingDataSource.setTargetDataSources(dataSourceMap);
        return routingDataSource;
    }

}

演示一下手工切换的代码:

public void init() {
    // 手工切换为数据源 first,初始化表
    RoutingDataSourceContext.setRoutingKey("first");
    createTableUser();
    RoutingDataSourceContext.reset();

    // 手工切换为数据源 second,初始化表
    RoutingDataSourceContext.setRoutingKey("second");
    createTableUser();
    RoutingDataSourceContext.reset();

}

这样就实现了最基本的多数据源切换了。

不难发现,切换工作很明显可以抽成一个切面,我们可以优化一下,利用注解标明切点,哪里需要切哪里。

三、引入AOP

自定义注解

/**
 * @author :Java课代表
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WithDataSource {
    String value() default "";
}

创建切面

@Aspect
@Component
// 指定优先级高于@Transactional的默认优先级
// 从而保证先切换数据源再进行事务操作
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class DataSourceAspect {

    @Around("@annotation(withDataSource)")
    public Object switchDataSource(ProceedingJoinPoint pjp, WithDataSource withDataSource) throws Throwable {

        // 1.获取 @WithDataSource 注解中指定的数据源
        String routingKey = withDataSource.value();
        // 2.设置数据源上下文
        RoutingDataSourceContext.setRoutingKey(routingKey);
        // 3.使用设定好的数据源处理业务
        try {
            return pjp.proceed();
        } finally {
            // 4.清空数据源上下文
            RoutingDataSourceContext.reset();
        }
    }
}

有了注解和切面,使用起来就方便多了:

// 注解标明使用"second"数据源
@WithDataSource("second")
public List<User> getAllUsersFromSecond() {
    List<User> users = userService.selectAll();
    return users;
}

关于切面有两个细节需要注意:

  1. 需要指定优先级高于声明式事务

    原因:声明式事务事务的本质也是 AOP,其只对开启时使用的数据源生效,所以一定要在切换到指定数据源之后再开启,声明式事务默认的优先级是最低级,这里只需要设定自定义的数据源切面的优先级比它高即可。

  2. 业务执行完之后一定要清空上下文

    原因:假设方法 A 使用@WithDataSource("second")指定走"second"数据源,紧跟着方法 B 不写注解,期望走默认的first数据源。但由于方法A放入上下文的lookupKey此时还是"second"并未删除,所以导致方法 B 执行的数据源与期望不符。

四、回顾

至此,基于AbstractRoutingDataSource+AOP的多数据源就实现好了。

在配置DataSource 这个Bean的时候,用的是自定义的RoutingDataSource,并且标记为 @Primary。这样就可以让mybatis-spring-boot-starter使用RoutingDataSource帮我们自动配置好mybatis,比搞两套DataSource+两套Mybatis配置的方案简单多了。

文中相关代码已上传课代表的github

特别说明:

样例中为了减少代码层级,让展示更直观,在 controller 层写了事务注解,实际开发中可别这么干,controller 层的任务是绑定、校验参数,封装返回结果,尽量不要在里面写业务!

五、优化

对于一般的多数据源使用场景,本文方案已足够覆盖,可以实现灵活切换。

但还是存在如下不足:

  • 每个应用使用时都要新增相关类,大量重复代码
  • 修改或新增功能时,所有相关应用都得改
  • 功能不够强悍,没有高级功能,比如读写分离场景下的读多个从库负载均衡

其实把这些代码封装到一个starter里面,高级功能慢慢扩展就可以。

好在开源世界早就有现成工具可用了,开发mybatis-plus的"baomidou"团队在其生态中开源了一个多数据源框架 Dynamic-Datasource,底层原理就是AbstractRoutingDataSource,增加了更多强悍的扩展功能,下篇介绍其使用。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Goby安装与使用

Goby安装与使用1.Goby简介1.1.Goby介绍1.2.Goby下载2.Goby使用2.1.切换语言2.2.新建扫描2.2.1.设置扫描地址2.2.2.设置端口2.2.2.1.选中默认端口2.2.2.2.自定义端口2.2.3.漏洞2.2.3.1.通用Poc2.2.3.2.暴力破解2.2.3.3.全部漏洞2.2.3.4.自定义Poc2.2.4.开始扫描2.3.扫描情况2.3.…

【Eureka】如何实现注册,续约,剔除,服务发现

文章目录前言服务注册服务续约服务剔除(服务端去剔除过期服务)被动下线服务下线&#xff08;主动下线&#xff09;client发起的服务发现集群同步信息Work下载前言 Eureka是SpringCloud的具体实现之一&#xff0c;提供了服务注册&#xff0c;发现&#xff0c;续约&#xff0c;撤…

[ Linux Audio 篇 ] Type-C 转 3.5mm音频接口介绍

简介 常见的Type-C 转3.5mm 线有两种&#xff1a; 模拟Type-C转3.5mm音频线数字Type-C转3.5mm 音频线&#xff0c;也就是带DAC芯片的转换线 当使用Type-C转换3.5mm音频接口时&#xff0c;使用到的是这里面的SBU1、D-、D、CC四个针脚&#xff0c;手机会通过这四个针脚输出模拟…

信贷--------

定义 信贷&#xff1a;一切以实现承诺为条件的价值运动方式&#xff0c;如贷款、担保、承诺、赊欠等 信贷业务&#xff1a;本外币贷款、贴现、透支、押汇&#xff08;表内信贷&#xff09;&#xff1b;票据承兑、信用证、保函、贷款承诺、信贷证明等&#xff08;表外信贷&…

卷积神经网络硬件加速——INT8数据精度加速

卷积神经网络硬件加速——INT8数据精度加速 上一专题已介绍了一种通用的卷积神经网络硬件加速方法——Supertile&#xff0c;本文将介绍一种特殊的硬件加速方案&#xff0c;一种INT8数据精度下的双倍算力提升方案。 目前大部分卷积神经网络模型的数据类型都是32-bits单精度浮点…

android开发笔记002

ListView控件 <ListViewandroid:id"id/main_iv"android:layout_width"match_parent"android:layout_height"match_parent"android:layout_below"id/main_top_layout"android:padding"10dp"android:divider"null&qu…

TCP三次握手详解

三次握手是 TCP 连接的建立过程。在握手之前&#xff0c;主动打开连接的客户端结束 CLOSE 阶段&#xff0c;被动打开的服务器也结束 CLOSE 阶段&#xff0c;并进入 LISTEN 阶段。随后进入三次握手阶段&#xff1a; ① 首先客户端向服务器发送一个 SYN 包&#xff0c;并等待服务…

c++11 标准模板(STL)(std::deque)(二)

构造函数 std::deque<T,Allocator>::deque deque(); (1) explicit deque( const Allocator& alloc ); (2)explicit deque( size_type count, const T& value T(), const Allocator& alloc Allocator());(3)(C11 前) …

网络编程 完成端口模型

1.概念以及重叠IO存在的问题 2.完成端口代码详解 整体流程 使用到的新函数 1.CreateIoCompletionPort函数 该函数创建输入/输出 (I/O) 完成端口并将其与指定的文件句柄相关联&#xff0c;或创建尚未与文件句柄关联的 I/O 完成端口&#xff0c;以便稍后关联&#xff0c;即创建…

金融业务的数据存储选型

为什么用关系型数据库&#xff1f;最常见的理由是别人在用&#xff0c;所以我也得用&#xff0c;但是这个并不是理由&#xff0c;而是借口。 1 数据分类 选择数据存储类型前&#xff0c;先分析数据特点&#xff0c;才能针对性选择存储方案。 通常按数据与数据之间关系的复杂…

SSM2---spring

Spring spring环境搭建 创建一个空白模块&#xff0c;目录结构如下 在pom.xml文件中引入相关依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/…

基于SSM(Spring+SpringMVC+Mybatis)实现的个人博客系统,含数据库文件及详细说明

关于项目 该博客是基于SSM实现的个人博客系统&#xff0c;适合初学SSM和个人博客制作的同学学习。 最新版本支持用户注册&#xff0c;包含用户和管理员两个角色 。 博主已写了一篇该项目的毕业论文和录制了2个小时的代码讲解可以供大家学习&#xff0c;需要的可以联系博主&…

RFID在模块管理中的应用

应用背景 模具是工业生产的基础工艺装备&#xff0c;被称为“工业之母”。作为国民经济的基础行业&#xff0c;模具涉及机械、汽车、轻工、电子、化工、冶金、建材等各个行业&#xff0c;应用范围十分广泛。模具资产管理采用传统的人工纸质记录的方式已经无法及时有效的进行&am…

还在用 XShell - 试试 IntelliJ IDEA 的 SSH

SSH 是很多人用得不多&#xff0c;但是又不得不用的工具。 如果你不是搞运维&#xff0c;没有必要搞个 CRT&#xff0c;XShell 也够用了&#xff0c;但是这 2 个都是收费软件&#xff0c;同时还不太便宜。 试试 IDEA 的 SSH 其实 IntelliJ IDEA 已经提供了 SSH 的功能。 如…

053-线程的状态改变及线程同步详细介绍

【上一讲】051-java线程的2种实现方法详解_CSDN专家-赖老师(软件之家)的博客-CSDN博客 线程可以处于以下四个状态之一1.新建(new):线程对象已经建立,但还没有启动,所以他不能运行。2.就绪(runnable):这种状态下,只要调度程序把时间片分配给线程就可以执行,也就是说…

10分钟带你了解什么是ArrayBuffer?

前言 很多时候我们前端开发是用不到 ArrayBuffer 的&#xff0c;但是用不到 ArrayBuffer 不代表我们不需要了解这个东西。本文就围绕 ArrayBuffer 来讲一下相关知识&#xff0c;大概需要10分钟左右就可以读完本文。 什么是ArrayBuffer&#xff1f; 描述 ArrayBuffer 对象用…

Paramiko库讲解

目录 基本概念 Paramiko组件架构 Key handing类 Transport类 SFTPClient类 SSHClient类—主要使用的类 Python编写完整例子 基本概念 Paramiko是Python实现SSHv2协议的模块&#xff0c;支持口令认证和公钥认证两种方式 通过Paramiko可以实现通过Python进行安全的远程命…

Html5网页播放器的同层播放功能

Html5网页播放器的同层播放功能&#xff1a; 在Android手机上使用H5播放视频时&#xff0c;大多数的国内浏览器厂商都会在视频播放时劫持<video>标签&#xff0c;使用浏览器自带的播放器播放视频&#xff0c;而且播放器会处于最高层级&#xff0c;视频上面无法显示其它h…

数影周报:字节跳动员工违规获取TikTok用户数据,阿里组织调整

本周看点&#xff1a;字节跳动员工违规获取TikTok用户数据&#xff1b;钉钉宣布用户数破6 亿&#xff1b;阿里组织调整&#xff1b;星尘数据完成 5000 万元 A 轮融资...... 数据安全那些事 字节跳动员工违规获取TikTok用户数据 字节跳动旗下热门应用TikTok日前曝出严重风波。字…

郭德纲落选,冯巩、赵炎上榜,国家非物质文化遗产传承人评选落幕

根据国家广电局26日消息&#xff0c;经过激烈的竞争&#xff0c;国家非物质文化遗产传承人评选工作&#xff0c;已经顺利落下帷幕。 在此次评选活动当中&#xff0c;评委会一致审议通过&#xff0c;著名相声演员冯巩和赵炎&#xff0c;被评为了非物质文化遗产传承人。而呼声很高…