【总结】在SpringBoot项目中如何动态切换数据源、数据库?(可直接CV)

注意:文章若有错误的地方,欢迎评论区里面指正 🍭 


前言

本文参考若依源码,介绍了如何在SpringBoot项目中使用AOP和自定义注解实现MySQL主从数据库的动态切换,当从库故障时,能自动切换到主库,确保服务的高可用性。

实现效果:如果服务器搭建的是一主多从多个mysql数据源,主服务器用来读。从服务器用来写。此时你在代码层面用注解指定了一个增删改方法到从数据源,但是碰巧此时从数据源失效了,那么就会自动的切换到其它服务器。


为什么要切换数据源,有哪些应用场景?

动态切换数据源通常是为了满足以下需求:

  1. 读写分离:在数据库架构中,为了提高性能和可用性,常常使用主从复制的方式。主数据库处理写操作,而从数据库处理读操作。动态切换数据源可以在不同的操作中使用不同的数据库,以达到优化性能的目的。

  2. 多租户架构:在SaaS(Software as a Service)应用中,不同的租户可能需要操作不同的数据库。动态数据源允许系统根据租户的身份来切换到对应的数据源。

  3. 分库分表:在处理大规模数据时,可能会采用分库分表的策略来分散数据存储的压力。动态切换数据源可以在执行跨库或跨表操作时,根据需要切换到正确的数据源。

  4. 环境隔离:在开发、测试和生产环境中,可能需要连接到不同的数据库。动态数据源可以在不同环境之间无缝切换,以确保数据的隔离和安全性。

  5. 灵活的数据库管理:在复杂的业务场景下,可能需要根据不同的业务逻辑来选择不同的数据源。动态数据源提供了这种灵活性,允许开发者根据运行时的条件来选择最合适的数据源。

  6. 故障转移和高可用性:当主数据库不可用时,动态切换数据源可以自动或手动切换到备用数据库,以保证服务的连续性和数据的可用性。

如何切换数据源?

SpringBoot版本:3.0.4

jdk版本:JDK17

1.pom文件

<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- aop切面--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!--druid连接池--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.20</version></dependency><!--mysql驱动--><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency><!--MybatisPlus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version></dependency>

2.配置文件:application.yml、application-druid.yml

application.yml配置文件:

#application.ymlserver:port: 8000
spring:profiles:active: druid

 application-druid.yml配置文件:

# 数据源配置
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.cj.jdbc.Driverdruid:# 主库数据源master:url: jdbc:mysql://localhost:3306/study?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: 123456# 从库数据源slave:# 从数据源开关/默认关闭enabled: trueurl: jdbc:mysql://localhost:3306/t_lyj?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: 123456# 初始连接数initialSize: 5# 最小连接池数量minIdle: 10# 最大连接池数量maxActive: 20# 配置获取连接等待超时的时间maxWait: 60000# 配置连接超时时间connectTimeout: 30000# 配置网络超时时间socketTimeout: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒timeBetweenEvictionRunsMillis: 60000# 配置一个连接在池中最小生存的时间,单位是毫秒minEvictableIdleTimeMillis: 300000# 配置一个连接在池中最大生存的时间,单位是毫秒maxEvictableIdleTimeMillis: 900000

 3、数据源名称枚举DataSourceType

/*** 数据源* * @author ruoyi*/
public enum DataSourceType
{/*** 主库*/MASTER,/*** 从库*/SLAVE
}

 4、Bean工具类SpringUtils

@Component
public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware 
{/** Spring应用上下文环境 */private static ConfigurableListableBeanFactory beanFactory;private static ApplicationContext applicationContext;@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {SpringUtils.beanFactory = beanFactory;}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {SpringUtils.applicationContext = applicationContext;}/*** 获取对象** @param name* @return Object 一个以所给名字注册的bean的实例* @throws BeansException**/@SuppressWarnings("unchecked")public static <T> T getBean(String name) throws BeansException{return (T) beanFactory.getBean(name);}/*** 获取类型为requiredType的对象** @param clz* @return* @throws BeansException**/public static <T> T getBean(Class<T> clz) throws BeansException{T result = (T) beanFactory.getBean(clz);return result;}/*** 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true** @param name* @return boolean*/public static boolean containsBean(String name){return beanFactory.containsBean(name);}/*** 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)** @param name* @return boolean* @throws NoSuchBeanDefinitionException**/public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException{return beanFactory.isSingleton(name);}/*** @param name* @return Class 注册对象的类型* @throws NoSuchBeanDefinitionException**/public static Class<?> getType(String name) throws NoSuchBeanDefinitionException{return beanFactory.getType(name);}/*** 如果给定的bean名字在bean定义中有别名,则返回这些别名** @param name* @return* @throws NoSuchBeanDefinitionException**/public static String[] getAliases(String name) throws NoSuchBeanDefinitionException{return beanFactory.getAliases(name);}/*** 获取aop代理对象* * @param invoker* @return*/@SuppressWarnings("unchecked")public static <T> T getAopProxy(T invoker){return (T) AopContext.currentProxy();}/*** 获取当前的环境配置,无配置返回null** @return 当前的环境配置*/public static String[] getActiveProfiles(){return applicationContext.getEnvironment().getActiveProfiles();}/*** 获取当前的环境配置,当有多个环境配置时,只获取第一个** @return 当前的环境配置*/public static String getActiveProfile(){final String[] activeProfiles = getActiveProfiles();return StringUtils.isNotEmpty(Arrays.toString(activeProfiles)) ? activeProfiles[0] : null;}/*** 获取配置文件中的值** @param key 配置文件的key* @return 当前的配置文件的值**/public static String getRequiredProperty(String key){return applicationContext.getEnvironment().getRequiredProperty(key);}
}

5、多数据源切换注解DataSource

/*** 自定义多数据源切换注解** 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准** @author lyj*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{/*** 切换数据源名称*/public DataSourceType value() default DataSourceType.MASTER;
}

 6、数据源解析配置类DruidConfig

@Configuration
public class DruidConfig {@Bean@ConfigurationProperties("spring.datasource.druid.master")public DataSource masterDataSource(DruidProperties druidProperties){DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return druidProperties.dataSource(dataSource);}@Bean@ConfigurationProperties("spring.datasource.druid.slave")@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")public DataSource slaveDataSource(DruidProperties druidProperties) {DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return druidProperties.dataSource(dataSource);}@Bean(name = "dynamicDataSource")@Primarypublic DynamicDataSource dataSource(DataSource masterDataSource) {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");return new DynamicDataSource(masterDataSource, targetDataSources);}/*** 设置数据源** @param targetDataSources 备选数据源集合* @param sourceName 数据源名称* @param beanName bean名称*/public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) {try {DataSource dataSource = SpringUtils.getBean(beanName);targetDataSources.put(sourceName, dataSource);} catch (Exception e) {}}
}

 7、数据源注入核心类DynamicDataSource

/*** 动态数据源* * @author lyj*/
public class DynamicDataSource extends AbstractRoutingDataSource {public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources){//设置默认数据源super.setDefaultTargetDataSource(defaultTargetDataSource);//设置目标数据源的映射super.setTargetDataSources(targetDataSources);//初始化super.afterPropertiesSet();}@Overrideprotected Object determineCurrentLookupKey(){return DynamicDataSourceContextHolder.getDataSourceType();}
}

8、数据源切换处理类DynamicDataSourceContextHolder

/*** 数据源切换处理* * @author lyj*/
public class DynamicDataSourceContextHolder
{public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);/*** 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。*/private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();/*** 设置数据源的变量*/public static void setDataSourceType(String dsType){log.info("切换到{}数据源", dsType);CONTEXT_HOLDER.set(dsType);}/*** 获得数据源的变量,默认使用主数据源*/public static String getDataSourceType(){return CONTEXT_HOLDER.get() == null ? DataSourceType.MASTER.name() : CONTEXT_HOLDER.get();}/*** 清空数据源变量*/public static void clearDataSourceType(){CONTEXT_HOLDER.remove();}
}

9、Aop切面类

@Aspect
@Order(1)
@Component
public class DataSourceAspect {@Pointcut("@annotation(com.LYJ.study.DynamicDataSource.annocation.DataSource)"+ "|| @within(com.LYJ.study.DynamicDataSource.annocation.DataSource)")public void dsPointCut(){}@Around("dsPointCut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable{DataSource dataSource = getDataSource(joinPoint);if (dataSource != null){DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());}try {return joinPoint.proceed();}finally {// 销毁数据源 在执行方法之后DynamicDataSourceContextHolder.clearDataSourceType();}}/*** 获取需要切换的数据源*/public DataSource getDataSource(ProceedingJoinPoint point){MethodSignature signature = (MethodSignature) point.getSignature();com.LYJ.study.DynamicDataSource.annocation.DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), com.LYJ.study.DynamicDataSource.annocation.DataSource.class);if (Objects.nonNull(dataSource)) {return dataSource;}return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);}
}

10、在业务中使用

@Service
@RequiredArgsConstructor
@DataSource(value=DataSourceType.MASTER)
//@DataSource(value=DataSourceType.SLAVE)
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {private final UserMapper userMapper;@Override@DataSource(value=DataSourceType.MASTER)//@DataSource(value=DataSourceType.SLAVE)public List<User> queryAll() {return userMapper.selectList(null);}
}

我们在service、mapper的类和方法上使用都可以。 

补充:有很多从数据源怎么办? 

我们上面已经配置了一个从数据源了,接下来我们继续配置多个从数据源

首先在application-druid.yml文件添加新的数据源

在枚举添加数据源名称


//如果配置多数据源,继续添加即可
public enum DataSourceType
{/*** 主库*/MASTER,/*** 从库*/SLAVE,/*** 从库2*/SLAVE2
}

 

如何切换数据库?

我们就以Oracle为例

 <!--oracle驱动--><dependency><groupId>com.oracle</groupId><artifactId>ojdbc6</artifactId><version>11.2.0.3</version></dependency>

在application-druid.yml添加

      slave3:# 从数据源开关/默认关闭enabled: trueurl: jdbc:oracle:thin:@127.0.0.1:1521:oracleusername: rootpassword: password

 然后删除指定的mysql驱动,默认会自动寻找驱动

添加数据源和用法参考上面即可,都是一样的。 

注意:在切换数据库时,因为mysql跟Oracle的sql语法有差别,启动时可能报错。

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

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

相关文章

1-什么是自动驾驶?

自动驾驶&#xff08;Autonomous Driving&#xff09;是指利用计算机系统来实现车辆的自主控制&#xff0c;使车辆能够在没有人类司机的情况下安全行驶的技术。自动驾驶车辆通常配备有多种传感器&#xff0c;如雷达&#xff08;Radar&#xff09;、激光雷达&#xff08;Lidar&a…

集群开发学习(三)修改用户模块(git 使用,局域网设代理)

git 使用 参考&#xff1a;https://blog.csdn.net/weixin_60033897/article/details/136016074 在服务器端创建公钥私钥 git config --global user.email "1686660735qq.com" git config --global user.name qinliangql git config --global -l # 查看信息# 这样可…

Java-内部类成员内部类

类的五大成员 属性 方法 构造方法 代码块 内部类 什么是内部类&#xff1f; 在一个类的里面&#xff0c;再定义一个类。 举例&#xff1a;在A类的内部定义B类&#xff0c;B类就被称为内部类 内部类表示的事物是外部类的一部分 内部类单独出现没有任何意义 内部类的访问特点 1.…

java中的this关键字—调用本类属性

在java中&#xff0c;当成员变量与局部变量发生重名问题时&#xff0c;需要使用this关键字分辨成员变量与局部变量 这里首先说使用this关键字调用本类中的属性 下面是一段代码 lass Student6{private String name;private int age;public Student6(String name,int age) {na…

YOLOv8模型代码学习

1.参考文献 链接1 2.网络模型解析 2.1卷积神经单元&#xff08;conv.py&#xff09; 在该文件中定义了yolov8网络中的卷积神经单元&#xff0c;位置如图所示。 def autopad(k, pNone, d1): # kernel(卷积核), padding(填充), dilation(扩张)"""Pad to same…

解决Java中的InstantiationException异常的技术指南

解决Java中的InstantiationException异常的技术指南 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 在Java编程中&#xff0c;InstantiationException异常是开…

Python中位运算详解

✨前言&#xff1a; 在Python中&#xff0c;位运算用于直接操纵数值的二进制位。以下是Python中的几种位运算符&#xff0c;每个位运算符的功能及其使用实例&#xff1a; ✨按位与&#xff08;AND&#xff09; - & 当两个操作数的位都为1时&#xff0c;结果的相应位才是…

Nginx日志管理之错误日志配置

Nginx 的错误日志可以帮助用户及时判断 Nginx 配置及运行时出错的原因&#xff0c;错误日志也可以通过 Nginx 内置指令进行配置&#xff0c;但不支持格式定义。配置指令如下表所示。 说明 错误日志指令组成主指令error_log作用域main、http、mail、stream、server、location默…

坐标传送指令

目录 主城传送 联盟 部落 中立 副本传送 旧世界&#xff08;Vanilla 1.0&#xff09; 燃烧的远征&#xff08;TBC 2.0&#xff09; 巫妖王之怒&#xff08;WLK 3.0&#xff09; 大灾变&#xff08;CTM 4.0&#xff09; 熊猫人之谜&#xff08;MOP 5.0&#xff09; 德…

Xlua三方库Android编译出错解决办法

Xlua三方库Android编译出错解决办法 最近听老师的热更教程&#xff0c;讲到xlua编译android平台会报错&#xff0c;也是看了老师的博客&#xff0c;按照方法去解决&#xff0c;然而问题并没有解决。应该是因为代码更新或者版本不一样&#xff0c;在此简单记录一下解决过程。 参…

Node.js 事件循环的工作流程

Node.js 的事件循环由六个主要阶段组成&#xff0c;每个阶段都有一个或多个回调队列。事件循环依次遍历这些阶段&#xff0c;并在每个阶段执行相应的回调函数。具体的流程如下&#xff1a; Timers&#xff08;计时器阶段&#xff09;&#xff1a; 执行已到期的 setTimeout 和 s…

js 获取get请求请求时间

要获取GET请求的响应时间&#xff0c;可以使用JavaScript的XMLHttpRequest对象。以下是一个示例&#xff1a; function getResponseTime(url, callback) {var xhr new XMLHttpRequest();var startTime new Date().getTime();xhr.onreadystatechange function() {if (xhr.re…

[RPI4] 树莓派4b安装istoreos及使用 -- 1. 系统安装

最近在研究家庭智能化的一些东西,其中包括网络,智能家居等一系列内容,然后看过的资料有的想再回来看的时候就找不到了,然后就想着开这么一个系列,做一些记录,先从智能家居开始吧。 1 安装istoreos系统 iStoreOS 目标是提供一个人人会用的路由兼轻 NAS 系统,不管是作为路…

Transformer预测 | 基于Transformer的风电功率多变量时间序列预测(Matlab)

文章目录 预测效果文章概述模型描述程序设计参考资料预测效果 文章概述 Transformer预测 | 基于Transformer的风电功率多变量时间序列预测(Matlab) Transformer 模型本质上都是预训练语言模型,大都采用自监督学习 (Self-supervised learning) 的方式在大量生语料上进行训练,…

python桌面应用

py文件 import osimport wx import wx.html2class MyFrame(wx.Frame):def __init__(self, parent):wx.Frame.__init__(self, parent, title"启动啦", size(1000, 700))# 创建一个Web视图组件self.browser wx.html2.WebView.New(self)# 加载本地HTML文件# self.brow…

考研数学|《李林880》正确率多少算合格?

李林880题是针对考研数学三的练习题集&#xff0c;覆盖了考研数学三的主要知识点和题型。如果能够熟练掌握这些题目&#xff0c;意味着对考研数学三的知识点有了较为深入的理解和应用能力。 首先&#xff0c;考研数学三的总分是150分&#xff0c;题型包括单选题、填空题和解答…

RN开发搬砖经验之—“Calculated frame index should never be lower than 0“崩溃问题分析

问题重现 崩溃堆栈&#xff1a; Back traces starts. java.lang.RuntimeException: java.lang.IllegalStateException: Calculated frame index should never be lower than 0at com.facebook.react.animated.NativeAnimatedModule$1.doFrameGuarded(NativeAnimatedModule.ja…

PostgreSQL中 GROUPING SETS、CUBE 和 ROLLUP功能

PostgreSQL 中&#xff0c;GROUPING SETS、CUBE 和 ROLLUP 的功能&#xff0c;允许在查询中更灵活地生成聚合结果&#xff0c;而不需要多次重写查询或使用复杂的 UNION 语句。这些功能在数据分析中特别有用&#xff0c;因为它们允许你以不同的维度对数据进行分组和聚合。 1、测…

持久化容器数据

持久化容器数据 目录 容器卷管理卷试一试 使用卷查看卷内容删除卷 当一个容器启动时&#xff0c;它会使用镜像提供的文件和配置。每个容器可以创建、修改和删除文件&#xff0c;并且不会影响其他容器。当容器被删除时&#xff0c;这些文件更改也会被删除。 虽然容器的这种短…

【因果推断python】53_效应异质性和非线性带来的挑战1

目录 Treatment Effects on Binary Outcomes 合成一些数据 由于缺乏基本事实&#xff0c;在单位层面预测治疗效果极为困难。因为我们只能观察到一个潜在结果 T(t) &#xff0c;我们无法直接估计它。相反&#xff0c;我们必须依靠目标变换&#xff08;也可以看作是设计巧妙的损…