SpringBoot第27讲:SpringBoot集成MySQL - MyBatis 多个数据源

SpringBoot第27讲:SpringBoot集成MySQL - MyBatis 多个数据源

本文是SpringBoot第27讲,在某些场景下,Springboot需要使用多个数据源,以及某些场景会需要多个数据源的动态切换。本文主要介绍上述场景及 SpringBoot+MyBatis实现多个数据源的方案和示例

文章目录

  • SpringBoot第27讲:SpringBoot集成MySQL - MyBatis 多个数据源
    • 1、知识准备
      • 1.1、什么场景会出现多个数据源?
      • 1.2、常见的多数据源的实现思路?
    • 2、简单示例
      • 2.1、分包方式实现
      • 2.2、针对场景二:主库和从库分离(读写分离)
    • 3、示例源码

1、知识准备

需要了解多数据源出现的场景和对应的多数据源集成思路。

1.1、什么场景会出现多个数据源?

一般而言有如下几种出现多数据源的场景。

  • 场景一:不同的业务涉及的表位于不同的数据库

随着业务的拓展,模块解耦,服务化的拆分等,不同的业务涉及的表会放在不同的数据库中。

  • 例如商品主库、商品审核库、商品附属库需要在一个微服务中使用;

  • daily环境和线上环境数据库、表、列、索引比对,同时需要访问daily环境和线上环境

  • 场景二:主库和从库分离(读写分离)

主从分离等相关知识请参考这篇文章: MySQL第七讲:MySQL分库分表详解

  • 场景三:数据库的分片

数据库的分片相关知识和方案请参考:SpringBoot集成MySQL - 分库分表ShardingJDBC

  • 场景四:多租户隔离

所有数据库表结构一致,只是不同客户的数据放在不同数据库中,通过数据库名对不同客户的数据隔离。这种场景有一个典型的叫法:多租户

PS:除了这种多租户除了用不同的数据库隔离不同客户数据外,还会通过额外的表字段隔离(比如tenant_id字段,不同的tenant_id表示不同的客户),对应的实现方式和案例可以参考 SpringBoot第30讲:SpringBoot集成MySQL - MyBatis-Plus基于字段隔离的多租户

1.2、常见的多数据源的实现思路?

应对上述出现的场景,多数据源方式如何实现呢?

  • 针对场景一:不同的业务涉及的表位于不同的数据库

    • 首先,出现这种场景且在一个模块中设计多数据源时,需要考虑当前架构的合理性,因为从设计的角度而言不同的业务拆分需要对应着不同的服务/模块
    • 其次,我们会考虑不同的package去隔离,不同的数据源放在不同的包下的代码中
  • 针对场景二:主库和从库分离(读写分离)

这种场景下我们叫动态数据源,通常方式使用AOP方式拦截+ThreadLocal切换。

2、简单示例

2.1、分包方式实现

分包方式实现:

1、在publish.properties中配置两个数据库:

# dbsource1
datasource.url=jdbc:mysql://xxx?useUnicode=true&characterEncoding=UTF-8&useAffectedRows=true&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
datasource.username=***
datasource.password=***
datasource.minIdle=5
datasource.maxActive=20

# dbsource2
datasource.daily.url=jdbc:mysql://***?useUnicode=true&characterEncoding=UTF-8&useAffectedRows=true&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
datasource.daily.username=***
datasource.daily.password=****
datasource.daily.minIdle=5
datasource.daily.maxActive=10

2、在application.yml中配置好映射关系

Spring:
 datasource1:
    url: ${datasource.url}
    username: ${datasource.username}
    password: ${datasource.password}
    minIdle: ${datasource.minIdle}
    maxActive: ${datasource.maxActive}

  # datasource config 2
  datasource2:
    url: ${datasource.daily.url}
    username: ${datasource.daily.username}
    password: ${datasource.daily.password}
    minIdle: ${datasource.daily.minIdle}
    maxActive: ${datasource.daily.maxActive}

3、建立连个数据源的配置文件:
第一个配置文件:

//表示这个类为一个配置类
@Configuration
// 配置mybatis的接口类放的地方
@MapperScan(basePackages = "com.mzd.multipledatasources.mapper.test01", sqlSessionFactoryRef = "test1SqlSessionFactory")
public class DataSourceConfig1 {
	// 将这个对象放入Spring容器中
	@Bean(name = "test1DataSource")
	// 表示这个数据源是默认数据源
	@Primary
	// 读取application.properties中的配置参数映射成为一个对象
	// prefix表示参数的前缀
	@ConfigurationProperties(prefix = "spring.datasource1")
	public DataSource getDateSource1() {
		return DataSourceBuilder.create().build();
	}
	@Bean(name = "test1SqlSessionFactory")
	// 表示这个数据源是默认数据源
	@Primary
	// @Qualifier表示查找Spring容器中名字为test1DataSource的对象
	public SqlSessionFactory test1SqlSessionFactory(@Qualifier("test1DataSource") DataSource datasource)
			throws Exception {
		SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
		bean.setDataSource(datasource);
		bean.setMapperLocations(
				// 设置mybatis的xml所在位置
				new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/test01/*.xml"));
		return bean.getObject();
	}
	@Bean("test1SqlSessionTemplate")
	// 表示这个数据源是默认数据源
	@Primary
	public SqlSessionTemplate test1sqlsessiontemplate(
			@Qualifier("test1SqlSessionFactory") SqlSessionFactory sessionfactory) {
		return new SqlSessionTemplate(sessionfactory);
	}
}

第二个配置文件:

@Configuration
@MapperScan(basePackages = "com.mzd.multipledatasources.mapper.test02", sqlSessionFactoryRef = "test2SqlSessionFactory")
public class DataSourceConfig2 {
    @Bean(name = "test2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource2")
    public DataSource getDateSource2() {
      	return DataSourceBuilder.create().build();
    }
    @Bean(name = "test2SqlSessionFactory")
    public SqlSessionFactory test2SqlSessionFactory(@Qualifier("test2DataSource") DataSource datasource)
        throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(datasource);
        bean.setMapperLocations(
            new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/test02/*.xml"));
        return bean.getObject();
    }
    @Bean("test2SqlSessionTemplate")
    public SqlSessionTemplate test2sqlsessiontemplate(
        @Qualifier("test2SqlSessionFactory") SqlSessionFactory sessionfactory) {
      			return new SqlSessionTemplate(sessionfactory);
    		}
}

注意:

  • 1、@Primary这个注解必须要加,因为不加的话spring将分不清楚哪个为主数据源(默认数据源)

  • 2、mapper的接口、xml形式以及dao层都需要两个分开,目录如图:

  • 在这里插入图片描述

  • 3、bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(“XXXX”)); mapper的xml形式文件位置必须要配置,不然将报错:no statement (这种错误也可能是mapper的xml中,namespace与项目的路径不一致导致的,具体看情况吧,注意一下就行,问题不大的)

  • 4、在service层中根据不同的业务注入不同的dao层。

  • 5、如果是主从复制- -读写分离:比如test01中负责增删改,test02中负责查询。但是需要注意的是负责增删改的数据库必须是主库(master)

  • 6、如果是分布式结构的话,不同模块操作各自的数据库就好,test01包下全是test01业务,test02全是test02业务,但是如果test01中掺杂着test02的编辑操作,这时候将会产生事务问题:即test01中的事务是没法控制test02的事务的,这个问题在之后的博客中会解决。

2.2、针对场景二:主库和从库分离(读写分离)

这种场景下我们叫动态数据源,通常方式使用AOP方式拦截+ThreadLocal切换。(本文的示例主要针对这种场景)

简介: 用这种方式实现多数据源的前提必须要清楚两个知识点:AOP原理和 AbstractRoutingDataSource抽象类。

1、AOP: 不切当的说就是相当于拦截器,只要满足要求的都会被拦截过来,然后进行一些列的操作。具体需要自己去体会。

2、AbstractRoutingDataSource: 这个类是实现多数据源的关键,他的作用就是动态切换数据源,

  • 实质:有多少个数据源就存多少个数据源在targetDataSources(是AbstractRoutingDataSource的一个map类型的属性,其中value为每个数据源,key表示每个数据源的名字)这个属性中,然后根据 determineCurrentLookupKey()这个方法获取当前数据源在map中的key值,然后determineTargetDataSource()方法中动态获取当前数据源,如果当前数据源不存并且默认数据源也不存在就抛出异常。

  • 存在就抛出异常。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    
  	//多数据源map集合
    private Map<Object, Object> targetDataSources;
    //默认数据源
    private Object defaultTargetDataSource;
    //其实就是targetDataSources,后面的afterPropertiesSet()方法会将targetDataSources 赋值给resolvedDataSources
    private Map<Object, DataSource> resolvedDataSources;
    private DataSource resolvedDefaultDataSource;
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource 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 + "]");
        } else {
            return dataSource;
        }
    }
    protected abstract Object determineCurrentLookupKey();
}

具体实现:

1、定义一个动态数据源: 继承AbstractRoutingDataSource 抽象类,并重写determineCurrentLookupKey()方法

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        DataSourceType.DataBaseType dataBaseType = DataSourceType.getDataBaseType();
        return dataBaseType;
    }
}

2、创建一个切换数据源类型的类: ThreadLocal这个知识点可以参考我的博客:JUC第六讲:ThreadLocal/InheritableThreadLocal详解/TTL-MDC日志上下文实践 就是为了线程的安全性,每个线程之间不会相互影响。

public class DataSourceType {

    public enum DataBaseType {
        TEST01, TEST02
    }

    // 使用ThreadLocal保证线程安全
    private static final ThreadLocal<DataBaseType> TYPE = new ThreadLocal<DataBaseType>();

    // 往当前线程里设置数据源类型
    public static void setDataBaseType(DataBaseType dataBaseType) {
        if (dataBaseType == null) {
            throw new NullPointerException();
        }
        System.err.println("[将当前数据源改为]:" + dataBaseType);
        TYPE.set(dataBaseType);
    }

    // 获取数据源类型
    public static DataBaseType getDataBaseType() {
        DataBaseType dataBaseType = TYPE.get() == null ? DataBaseType.TEST01 : TYPE.get();
        System.err.println("[获取当前数据源的类型为]:" + dataBaseType);
        return dataBaseType;
    }

    // 清空数据类型
    public static void clearDataBaseType() {
      	TYPE.remove();
    }
}

3、定义多个数据源: 怎么定义就不多说了,和方法一是一样的,主要是将定义好的多个数据源放在动态数据源中。

@Configuration
@MapperScan(basePackages = "com.mzd.multipledatasources.mapper", sqlSessionFactoryRef = "SqlSessionFactory")
public class DataSourceConfig {
	@Primary
	@Bean(name = "test1DataSource")
	@ConfigurationProperties(prefix = "spring.datasource1")
	public DataSource getDateSource1() {
			return DataSourceBuilder.create().build();
	}
	@Bean(name = "test2DataSource")
	@ConfigurationProperties(prefix = "spring.datasource.test2")
	public DataSource getDateSource2() {
			return DataSourceBuilder.create().build();
	}

	@Bean(name = "dynamicDataSource")
	public DynamicDataSource DataSource(@Qualifier("test1DataSource") DataSource test1DataSource,
			@Qualifier("test2DataSource") DataSource test2DataSource) {
      Map<Object, Object> targetDataSource = new HashMap<>();
      targetDataSource.put(DataSourceType.DataBaseType.TEST01, test1DataSource);
      targetDataSource.put(DataSourceType.DataBaseType.TEST02, test2DataSource);
      DynamicDataSource dataSource = new DynamicDataSource();
      dataSource.setTargetDataSources(targetDataSource);
      dataSource.setDefaultTargetDataSource(test1DataSource);
      return dataSource;
	}
	@Bean(name = "SqlSessionFactory")
	public SqlSessionFactory test1SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
			throws Exception {
		SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
		bean.setDataSource(dynamicDataSource);
		bean.setMapperLocations(
				new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml"));
		return bean.getObject();
	}
}

4、定义AOP: 就是不同业务切换不同数据库的入口。如果觉得execution太长不愿意写,就可以定义一个注解来实现。可参考于我的博客:Java基础知识第二讲:Java开发手册/注解/反射/ IO

@Aspect
@Component
public class DataSourceAop {
	@Before("execution(* com.mzd.multipledatasources.service..*.test01*(..))")
	public void setDataSource2test01() {
		System.err.println("test01业务");
		DataSourceType.setDataBaseType(DataBaseType.TEST01);
	}
	
	@Before("execution(* com.mzd.multipledatasources.service..*.test02*(..))")
	public void setDataSource2test02() {
		System.err.println("test02业务");
		DataSourceType.setDataBaseType(DataBaseType.TEST02);
	}
}

整体目录如图:
在这里插入图片描述

3、示例源码

todo

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

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

相关文章

数据湖真的能取代数据仓库吗?【SNP SAP数据转型 】

数据湖和数据仓库的存在并不冲突&#xff0c;也并不是取代的关系&#xff0c;而是相互的融合关系。 数据湖是近两年中比较新的技术在大数据领域中&#xff0c;对于一个真正的数据湖应该是什么样子&#xff0c;现在对数据湖认知还是处在探索的阶段&#xff0c;像现在代表的开源产…

[SUCTF2019]hardcpp

前言 又遇到ollvm了 解混淆 可以直接用angr运行脚本去除除控制流平坦化&#xff0c;最好在ancoda等管理环境里面安装angr不然问题很多 https://github.com/Pure-T/deflat 去除前 去除后&#xff0c;它将多余的直接nop了 分析 主要加密区域位于匿名函数这一块&#xff0c…

Kafka 小结

Kafka 是由 Linkedin 开发并开源的分布式消息系统&#xff0c;因其分布式及高吞吐率而被广泛使用&#xff0c;现已与 Cloudera Hadoop、Apache Storm、Apache Spark、Flink 集成。 Kafka 使用场景 页面访问量 PV、页面曝光 Expose、页面点击 Click 等行为事件&#xff1b;实时计…

uniapp中uni-popup的用法——实例讲解

uni-pop弹出层组件&#xff0c;在应用中弹出一个消息提示窗口、提示框等,可以设置弹出层的位置&#xff0c;是中间、底部、还是顶部。 如下图效果所示&#xff1a;白色区域则为弹出的pop层。 一、 创建一个自定义组件&#xff1a; 1.项目中安装下载uni-pop插件。 2.把pop内容…

同一局域网内IP 192.168.1.10 和 IP 10.10.10.8 可以互相访问吗?

同一局域网内IP 192.168.1.10 和 IP 10.10.10.8 可以互相访问吗&#xff1f; 1、网上邻居的方式&#xff1a; 鼠标点击 我的电脑 属性 计算机名&#xff0c;查看一下 计算机名&#xff08;这个可以点击更改&#xff0c;自己设定和更改&#xff09; 查看一下工作组&#xff0c;一…

Python———PyCharm下载和安装

&#xff08;一&#xff09;开发环境介绍 开发环境&#xff0c;英文是 IDE &#xff08; Integrated Development Environment 集成开发环境&#xff09;。 不要纠结于使用哪个开发环境。开发环境本质上就是对Python 解释器python.exe 的封装&#xff0c;核心都一样。可以说&…

PostgreSQL使用localhost可以连接,使用IP无法连接

问题描述&#xff1a;PostgreSQL使用localhost可以连接&#xff0c;使用IP无法连接 默认情况下&#xff0c;刚安装完成的 postgresSQL12 无法使用 数据库连接工具&#xff08;如postman&#xff09;连接。需要为其修改配置&#xff0c;开放连接权限。 修改pg_hba.conf 增加…

采用VMD按照某一坐标轴旋转坐标结构

关注 M r . m a t e r i a l , \color{Violet} \rm Mr.material\ , Mr.material , 更 \color{red}{更} 更 多 \color{blue}{多} 多 精 \color{orange}{精} 精 彩 \color{green}{彩} 彩&#xff01; 主要专栏内容包括&#xff1a; †《LAMMPS小技巧》&#xff1a; ‾ \textbf…

深入理解深度学习——BERT派生模型:BART(Bidirectional and Auto-Regressive Transformers)

分类目录&#xff1a;《深入理解深度学习》总目录 UniLM和XLNet都尝试在一定程度上融合BERT的双向编码思想&#xff0c;以及GPT的单向编码思想&#xff0c;同时兼具自编码的语义理解能力和自回归的文本生成能力。由脸书公司提出的BART&#xff08;Bidirectional and Auto-Regre…

java文件夹上传,保留文件夹结构

需求: 产品要求可以上传文件夹,文件夹下包含其他文件夹 前端上传文件夹,可以把文件以及所在文件所在文件夹信息传到后端 1.前端设置 需要设置 webkitdirectory enctype multipart/form-data <!DOCTYPE html> <html> <head><meta charset"UTF-8&…

应用层:动态主机配置协议(DHCP)

1.应用层&#xff1a;动态主机配置协议(DHCP) 笔记来源&#xff1a; 湖科大教书匠&#xff1a;应用层概述 湖科大教书匠&#xff1a;动态主机配置协议(DHCP) 声明&#xff1a;该学习笔记来自湖科大教书匠&#xff0c;笔记仅做学习参考 如何配置用户主机才能使用户主机正常访问…

【数据科学赛】2023大模型应用创新挑战赛 #¥10万 #百度

CompHub 主页增加了“近两周上新的奖金赛”&#xff0c;更加方便查找最新比赛&#xff0c;欢迎访问和反馈&#xff01; 以下内容摘自比赛主页&#xff08;点击文末阅读原文进入&#xff09; Part1赛题介绍 题目 2023大模型应用创新挑战赛 举办平台 Baidu AI Studio 主办方…

Java设计模式之一:观察者模式

目录 一、什么是观察者模式 二、如何使用观察者模式 三、观察者模式的优势和使用场景 一、什么是观察者模式 观察者模式是一种常见的设计模式&#xff0c;用于在对象之间建立一对多的依赖关系。在该模式中&#xff0c;一个主题&#xff08;被观察者&#xff09;维护了一个观…

力扣 93. 复原 IP 地址

题目来源&#xff1a;https://leetcode.cn/problems/restore-ip-addresses/description/ C题解&#xff1a;递归回溯法。 递归参数&#xff1a;因为不能重复分割&#xff0c;需要ind记录下一层递归分割的起始位置&#xff1b;还需要一个变量num&#xff0c;记录ip段的数量。递…

陪诊小程序系统|陪诊软件开发|陪诊系统功能和特点

随着医疗服务的逐步改善和完善&#xff0c;越来越多的人群开始走向医院就诊&#xff0c;而其中不少人往往需要有人陪同前往&#xff0c;这就导致了许多矛盾与问题的发生&#xff0c;比如长时间等待、找不到合适的陪诊人员等。因此为人们提供一种方便快捷的陪诊服务成为了一种新…

【实战】 二、React 与 Hook 应用:实现项目列表 —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二)

文章目录 一、项目起航&#xff1a;项目初始化与配置二、React 与 Hook 应用&#xff1a;实现项目列表1.新建文件2.状态提升3.新建utils4.Custom Hook 学习内容来源&#xff1a;React React Hook TS 最佳实践-慕课网 相对原教程&#xff0c;我在学习开始时&#xff08;2023.0…

ClickHouse主键索引最佳实践

在本文中&#xff0c;我们将深入研究ClickHouse索引。我们将对此进行详细说明和讨论&#xff1a; ClickHouse的索引与传统的关系数据库有何不同ClickHouse是怎样构建和使用主键稀疏索引的ClickHouse索引的最佳实践 您可以选择在自己的机器上执行本文给出的所有Clickhouse SQL…

SQlite数据库

SQlite数据库 1.SQLite简介 轻量化&#xff0c;易用的嵌入式数据库&#xff0c;用于设备端的数据管理&#xff0c;可以理解成单点的数据库。传统服务器型数据库用于管理多端设备&#xff0c;更加复杂 SQLite是一个无服务器的数据库&#xff0c;是自包含的。这也称为嵌入式数…

2020年国赛高教杯数学建模C题中小微企业的信贷决策解题全过程文档及程序

2020年国赛高教杯数学建模 C题 中小微企业的信贷决策 原题再现 在实际中&#xff0c;由于中小微企业规模相对较小&#xff0c;也缺少抵押资产&#xff0c;因此银行通常是依据信贷政策、企业的交易票据信息和上下游企业的影响力&#xff0c;向实力强、供求关系稳定的企业提供贷…

Win10电脑开机PIN码怎么取消?

有的用户稀里糊涂的设置了PIN码之后&#xff0c;在开机时发现多了个PIN码&#xff0c;但又不知道电脑PIN码是什么意思&#xff0c;也不清楚开机PIN码怎么取消。您可以通过阅读以下内容&#xff0c;以了解什么是PIN以及如何取消PIN码。 PIN码是一种快捷登录密码方式&#xff0c;…