SpringBoot实现多数据源(一)【普通版切换】

news2025/1/23 13:05:55

在实际开发中,经常可能遇到在一个应用中可能需要访问多个数据库的情况。以下是两种典型场景

业务复杂(数据量大)

数据分布在不同的数据库中,数据库拆了,应用没拆。一个公司多个子项目,各用各的数据库,设计数据共享…

在这里插入图片描述

读写分离

为解决 数据库的读性能瓶颈读比写性能更高,写锁会影响读阻塞,从而影响读的性能

很多数据库拥有主从架构,也就是说,一台主数据库服务器,是对外提供增删改业务的生产服务;另一(多)台从数据库服务器,主要进行读的操作

可以通过中间件(ShardingSphere、mycat、mysql-proxy、TDDL…),但是有一些规模较小的公司,没有专门的中间件团队搭建读写分离基础设施,因此需要业务开发人员自行实现读写分离

在这里插入图片描述

这里的架构与上图类似,不同的是,在读写分离中,主库和从库的数据库是一致的(不考虑主从延迟)。数据更新操作(insert、update、delete)都是在主库上进行,主库将数据变更信息同步给从库。在查询时,可以在从库上进行,从而分担主库的压力

一、实现多数据源


对于大多数 Java 应用,都使用了 Spring 框架,spring-jdbc 模块提供了 AbstractRoutingDataSource,其内部可以包含多个 DataSource,然后在运行时来动态的访问哪个数据库。这种方式访问数据库的架构如下图所示

在这里插入图片描述

测试样例一:普通的动态数据源

  1. 创建两个数据库readwrite
CREATE DATABASE `read` /*!40100 DEFAULT CHARACTER SET utf8 */
CREATE TABLE `people` (
  `name` VARCHAR(50) DEFAULT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE DATABASE `write` /*!40100 DEFAULT CHARACTER SET utf8 */
CREATE TABLE `people` (
  `name` VARCHAR(50) DEFAULT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8;
  1. 创建一个SpringBoot 项目 dynamic_datasource

  2. 导入依赖

    • pom.xml
<dependencies>
    <!--jdbc-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--mybatis-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
    </dependency>
    <!--druid-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.8</version>
    </dependency>
    <!--mysql驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.28</version>
    </dependency>
    <!--lombok-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</version>
    </dependency>
</dependencies>
  1. 应用配置文件
    • application.yaml
spring:
  application:
    name: dynamic_datasource
  # 数据源
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    # 读数据源
    read:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/read?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
      username: root
      password: 123456
    # 写数据源
    write:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/write?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
      username: root
      password: 123456
# 端口号
server:
  port: 3355

# 别名、xml文件配置
mybatis:
  mapper-locations: classpath:com/vinjcent/mapper/**/*.xml
  type-aliases-package: com.vinjcent.pojo
  1. 实体类
    • People
package com.vinjcent.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class People {

    private String name;

}
  1. 数据源配置类

    • DataSourceConfiguration
    package com.vinjcent.config;
    
    import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    @Configuration
    public class DataSourceConfiguration {
    
        @Bean(name = "readDatasource")
        @ConfigurationProperties(prefix = "spring.datasource.read")
        public DataSource readDatasource() {
            // 底层会自动拿到spring.datasource中的配置,创建一个DruidDataSource
            return DruidDataSourceBuilder.create().build();
        }
    
        @Bean(name = "writeDatasource")
        @ConfigurationProperties(prefix = "spring.datasource.write")
        public DataSource writeDatasource() {
            // 底层会自动拿到spring.datasource中的配置,创建一个DruidDataSource
            return DruidDataSourceBuilder.create().build();
        }
    }
    
  2. 动态数据源

    • DynamicDataSource
package com.vinjcent.config;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;


@Component
@Primary    // 将该Bean设置为主要注入Bean
public class DynamicDataSource implements DataSource, InitializingBean {

    // 用于存储数据源的标识
    public static ThreadLocal<String> name = new ThreadLocal<>();

    // 写
    private final DataSource writeDataSource;

    // 读
    private final DataSource readDataSource;

    @Autowired
    public DynamicDataSource(@Qualifier("readDatasource") DataSource readDataSource,
                             @Qualifier("writeDatasource") DataSource writeDataSource) {
        this.readDataSource = readDataSource;
        this.writeDataSource = writeDataSource;
    }

    @Override
    public Connection getConnection() throws SQLException {
        return name.get().equals("w") ? writeDataSource.getConnection() : readDataSource.getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return null;
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return null;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {

    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {

    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }

    /**
     * 初始化bean的initialization接口
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        // TODO 初始化
        name.set("w");
    }
}
  1. 测试接口
    • PeopleController
package com.vinjcent.controller;

import com.vinjcent.config.DynamicDataSource;
import com.vinjcent.pojo.People;
import com.vinjcent.service.PeopleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("people")
public class PeopleController {

    private final PeopleService peopleService;

    @Autowired
    public PeopleController(PeopleService peopleService) {
        this.peopleService = peopleService;
    }


    @GetMapping("/list")
    public List<People> getAllPeople() {
        // 修改对应数据源
        DynamicDataSource.name.set("r");
        return peopleService.list();
    }

    @GetMapping("/insert")
    public String addPeople() {
        // 修改对应数据源
        DynamicDataSource.name.set("w");
        peopleService.save(new People("vinjcent"));
        return "添加成功";
    }

}

  1. 主启动类(需要排除掉SpringBoot数据源自动配置类,否则会报错
package com.vinjcent;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@MapperScan("com.vinjcent.mapper")
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)     // 排除SpringBoot数据源自动配置类
public class DynamicDatasourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(DynamicDatasourceApplication.class, args);
    }

}
  1. 测试访问接口,查看是否读写分离

测试实例二:实现 AbstractRoutingDataSource

应用直接操作的是 AbstractRoutingDataSource 的实现类,告诉 AbstractRoutingDataSource 访问哪个数据库,然后由 AbstractRoutingDataSource 从事先配好的数据源(readDataSource、writeDataSource)选择一个,来访问对应的数据库

在这里插入图片描述

流程

  • 当执行数据库持久化操作,只要继承了 Spring 就一定会通过 DataSourceUtils 获取 Connection
  • 通过 Spring 注入的 DataSource 获取 Connection 即可执行数据库操作
    • 所以思路就是:只需配置一个 DataSource 的 bean,然后根据业务动态提供 Connection 即可
  • 其实 Spring 已经提供了一个 DataSource 实现类用于动态切换数据源—AbstractRoutingDataSource
  • 分析 AbstractRoutingDataSource 即可实现动态数据源切换

AbstractRoutingDataSource 分析

// targetDataSources 保存了key和数据库连接的映射关系
private Map<Object, Object> targetDataSources;
// 标识默认的连接
private Object defaultTargetDataSource;
// 这个数据结构是通过 targetDataSources 构建而来,存储结构也是数据库标识和数据源的映射关系
private Map<Object, DataSource> resolvedDataSources;

AbstractRoutingDataSource 实现了 InitializingBean 接口,并实现了 afterPropertiesSet() 方法。afterPropertiesSet() 方法是初始化bean的时候执行,通常用作数据初始化。resolvedDataSources 就是在这里赋值

public void afterPropertiesSet() {
    if (this.targetDataSources == null) {
        throw new IllegalArgumentException("Property 'targetDataSources' is required");
    } else {
        this.resolvedDataSources = new HashMap(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = this.resolveSpecifiedLookupKey(key);
            DataSource dataSource = this.resolveSpecifiedDataSource(value);
            this.resolvedDataSources.put(lookupKey, dataSource);
        });
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }

    }
}
  • 因此,我们只需要创建 AbstractRoutingDataSource 实现类 DynamicDataSource ,然后初始化 targetDataSources 和key为数据源的标识(可以是枚举、字符串都行,因为标识是Object类型)
  • 后续当调用 AbstractRoutingDataSource.getConnection 会接着调用提供的模板方法:determineTargetDataSource
  • 通过 determineDataSource 该方法返回的数据库标识,从 resolvedDataSources 中拿到对应的数据源(类似于一个策略模式
  • 所以最终,只需要 DynamicDataSource 中实现 determineTargetDataSource 为其提供一个数据库标识
  1. 修改 DynamicDataSource 动态数据源类
package com.vinjcent.config;

import com.vinjcent.constants.DataSourceConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

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


@Component
@Primary    // 将该Bean设置为主要注入Bean
public class DynamicDataSource extends AbstractRoutingDataSource {

    // 用于存储数据源的标识
    public static ThreadLocal<String> name = new ThreadLocal<>();

    // 写
    private final DataSource writeDataSource;

    // 读
    private final DataSource readDataSource;


    @Autowired
    public DynamicDataSource(@Qualifier("readDatasource") DataSource readDataSource,
                             @Qualifier("writeDatasource") DataSource writeDataSource) {
        this.readDataSource = readDataSource;
        this.writeDataSource = writeDataSource;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return name.get();
    }

    // 初始化完bean之后调用该方法
    @Override
    public void afterPropertiesSet() {
        // 为targetDataSources 初始化所有数据源
        Map<Object, Object> sources = new HashMap<>();
        sources.put(DataSourceConstants.READ_DATASOURCE, readDataSource);
        sources.put(DataSourceConstants.WRITE_DATASOURCE, writeDataSource);
        super.setTargetDataSources(sources);

        // 为 defaultTargetDataSource 设置默认的数据源
        super.setDefaultTargetDataSource(readDataSource);

        // resolvedDataSources 负责最终切换的数据源map
        super.afterPropertiesSet();
    }
}
  1. 添加数据源常量类
    • DataSourceConstants
package com.vinjcent.constants;

public class DataSourceConstants {

    // 读数据源
    public static final String READ_DATASOURCE = "read";

    // 写数据源
    public static final String WRITE_DATASOURCE = "write";
}
  1. 修改 PeopleController 类
package com.vinjcent.controller;

import com.vinjcent.config.DynamicDataSource;
import com.vinjcent.constants.DataSourceConstants;
import com.vinjcent.pojo.People;
import com.vinjcent.service.PeopleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("people")
public class PeopleController {

    private final PeopleService peopleService;

    @Autowired
    public PeopleController(PeopleService peopleService) {
        this.peopleService = peopleService;
    }


    @GetMapping("/list")
    public List<People> getAllPeople() {
        // 修改对应数据源
        DynamicDataSource.name.set(DataSourceConstants.READ_DATASOURCE);
        return peopleService.list();
    }

    @GetMapping("/insert")
    public String addPeople() {
        // 修改对应数据源
        DynamicDataSource.name.set(DataSourceConstants.WRITE_DATASOURCE);
        peopleService.save(new People("vinjcent"));
        return "添加成功";
    }

}
  1. 测试访问接口,查看是否读写分离

下一篇文章《SpringBoot实现多数据源(二)【Mybatis插件】

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

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

相关文章

Springboot——拦截器

目录 一、拦截器概念 二、拦截器的使用 2.1 拦截器的创建&#xff08;preHandle实用性最强&#xff09; 2.2 将拦截器添加到容器当中 三、拦截器参数 3.1 获取请求头 request.getHeader 3.2 Object handler 是什么参数 3.3 ModelAndView modelAndView 3.4 Exception ex 3.…

多重定义的全局符号,链接器会如何链接的情况

多重定义的全局符号&#xff0c;链接器会如何链接的情况实例1&#xff1a;1.规则12.规则13.规则24.规则3实例2总结以下只针对于gcc编译器&#xff0c;而且不同环境&#xff0c;不同编译器的情况可能不同。 假如说有多重定义的全局符号&#xff0c;链接器会如何链接呐&#xff…

[论文评析]AdaptivePose: Human Parts as Adaptive Points,AAAI 2022

AdaptivePose: Human Parts as Adaptive Points文章信息背景AdaptivePose身体表示方法Body RepresentationAdaptivePosePart Perception ModuleEnhanced Center-aware BranchTwo-hop Regression BranchLoss function推理Inference总结References文章信息 论文题目&#xff1a;…

Spring Boot JPA 存储库派生查询示例

在之前的文章中&#xff0c;您已经知道如何使用JPQL和本机查询通过注释从数据库中检索数据。今天&#xff0c;我将向您展示如何使用派生查询方法在 Spring 引导中实现 Spring Data JPA 存储库查询&#xff1a;Query 派生查询方法的结构配置 Spring 引导应用程序以使用不同的数…

【教学类-19-02】20221127《ABCABC式-规律排序-A4竖版2份》(中班)

展示效果&#xff1a; 单人使用样式&#xff1a; 单页打印样式 ​ 背景需求&#xff1a; 中班幼儿需要掌握ABCABC的排序规律 ​ 前文制作了ABAB单元格色块&#xff0c;微调word表格的列数&#xff0c;调整python的参数&#xff0c;随机生成ABC排序样式&#xff0c;引导幼儿…

Android 单ABI架构适配指南:保姆级教学 INSTALL_FAILED_NO_MATCHING_ABIS

单ABI架构 64位 安装报错误详情如下 &#xff1a; Installation did not succeed. The application could not be installed: INSTALL_FAILED_NO_MATCHING_ABIS 产品需求&#xff1a; 单ABI架构支持拆解 迎合市场需求 32/64位独立包 目前项目ABI架构如下&#xff1a; …

基于Mxnet实现语义分割-整体多模型【完整、附部分源码】

文章目录前言语义分割发展史及意义一、数据集的准备二、基于Mxnet的语义分割框架构建1.引入库2.CPU/GPU配置3.数据标准化4.解析数据集到列表中JSON格式Label 图像的标注格式5.设置数据迭代器6.模型构建fcn模型结构pspnet模型结构deeplabv3模型结构deeplabv3模型结构ICNet模型结…

刷爆力扣之构建乘积数组

刷爆力扣之构建乘积数组 HELLO&#xff0c;各位看官大大好&#xff0c;我是阿呆 &#x1f648;&#x1f648;&#x1f648; 今天阿呆继续记录下力扣刷题过程&#xff0c;收录在专栏算法中 &#x1f61c;&#x1f61c;&#x1f61c; 该专栏按照不同类别标签进行刷题&#xff0c…

合作对策模型的简单实现

以如下题目作为示例&#xff1a; 一位歌手(S)&#xff0c;一位钢琴家 (P) 和一位鼓手(D) 组成一个小乐队在俱乐部同台演出能得到演出费1000元&#xff0c;若歌手和钢琴家一起演出能得800元。而只有钢琴家和鼓手一起演出能得到650元&#xff0c;钢琴独奏表演能得300元&#xff…

表单与列表在HTML与CSS中是这么玩的

文章目录表单应用场景实例讲解表单描述标签的使用 label表单约束属性详解表单访问限制技巧常用字段类型扩展隐藏与表单提交技巧表单验证使用总结大文本与列表框技巧详解选项框标准打开方式文件上传的正确打开方式时期与时间表单项使用详解搜索表单与DATALIST数据列表表单历史数…

【Java多线程】线程状态及线程方法大全

➤ Java多线程编程【一文全解】 文章目录线程状态线程方法> 停止线程 stop( )> 线程休眠 sleep( )> 线程礼让 yield( )> 线程强行执行 join( )> 线程状态观测 Thread.State> 线程的优先级 Priority> 守护多线程 daemon线程状态 线程有五大状态: 创建状态…

Java代码审计——XML 外部实体注入(XXE)

目录 前言&#xff1a; &#xff08;一&#xff09;XML 的常见接口 1&#xff0e;XMLReader 2&#xff0e;SAXBuilder 3&#xff0e;SAXReader 4&#xff0e;SAXParserFactory 5&#xff0e;Digester 6&#xff0e;DocumentBuilderFactory (二&#xff09;XXE 漏洞审计…

MongoDB的安装

配置环境变量,将F:\MongoDB\Server\bin的路径添加到PATH环境变量中配置环境变量,将F:\MongoDB\Server\bin的路径添加到PATH环境变量中 1、下载 在安装数据库之前将所有杀毒软件、防护软件全部关闭掉 官网下载地址&#xff1a;Download MongoDB Community Server | MongoDB …

【软件测试】测试人我明明测了,生产环境还出问题?又出幺蛾子......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 测试人对于线上问题…

大数据_数据中台建设与架构

目录 一、持续让数据用起来的机制框架 二、数据中台建设方法论 三、数据中台架构 一、持续让数据用起来的机制框架 数据中台的使命就是持续让数据用起来&#xff0c;它的根本性特点就是把“数据资产”作为基础要素独立出来&#xff0c;让成为资产的数据作为生产资料融入业务…

【SVM分类】基于matlab鸽群算法优化支持向量机SVM分类【含Matlab源码 2242期】

⛄一、鸽群算法简介 基于鸽群在归巢过程中的特殊导航行为,Duan等提出了一种仿生群体智能优化算法———鸽群优化算法.在这个算法中,通过模仿鸽子在寻找目标的不同阶段使用不同导航工具这一机制,提出了2种不同的算子模型: 1)地图和指南针算子(map and compass operator).鸽子可…

ElasticSearch入门

1. ELASTICSEARCH 1、安装elastic search dokcer中安装elastic search &#xff08;1&#xff09;下载ealastic search和kibana docker pull elasticsearch:7.6.2 docker pull kibana:7.6.2&#xff08;2&#xff09;配置 mkdir -p /mydata/elasticsearch/config mkdir -p…

day01 Redis

day01 Redis 第一章 非关系型数据库的简介 第一节 技术发展线路 第二节 互联网发展所面临的问题 1. Web1.0 时代 2. Web2.0时代 3. 互联网三高问题 3.1 高并发、大流量 大型网站系统需要面对高并发(QPS)用户&#xff0c;大流量访问。Google日均PV数35亿&#xff0c;日均IP…

centos8.4下搭建 rocketmq集群部署 4.9.4

1.简单介绍 搭建rocketmq集群,nameserver至少3个节点&#xff0c;brokerserver采用2主2从同步&#xff0c;服务器资源多的&#xff0c;可以至少部署在7台服务器上&#xff0c;资源少的可以准备至少3台服务器 172.16.4.15nameserver172.16.4.16nameserver172.16.4.17nameserver…

三维种子点生长算法(以及Python递归深度问题)

前言 种子点生长算法是从一个种子点向周围的遍历图像的每个像素的图遍历算法。 通常在二维中有8邻域方法&#xff0c;三维中有6邻域与26邻域方法。 本文实现了三维种子点生长的6邻域算法。 种子点生长算法本质上是对图像的连通部分进行遍历&#xff0c;因此可以分别利用深度优…