在Spring Boot项目中通过自定义注解实现多数据源以及主备数据库切换

在现代的企业应用开发中,使用多数据源是一个常见的需求。尤其在关键应用中,设置主备数据库可以提高系统的可靠性和可用性。在这篇博客中,我将展示如何在Spring Boot项目中通过自定义注解实现多数据源以及主备数据库切换。

在此说明

我这里以dm6、dm7来举例多数据源 ,以两个dm6来举例主备数据库,基本大部分数据库都通用,举一反三即可。

对于dm6不熟悉但是又要用的可以看我这篇博客

Spring Boot项目中使用MyBatis连接达梦数据库6

1. 环境依赖

首先,确保你的Spring Boot项目中已经添加了以下依赖:

  <!-- Lombok依赖,用于简化Java代码 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- MyBatis Spring Boot Starter依赖,用于集成MyBatis和Spring Boot --><!-- 注意:这里使用1.3.0版本,因为DM6不支持1.3以上版本 --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.0</version></dependency><!-- Spring Boot Starter AOP依赖,用于实现AOP功能 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!-- DM6 JDBC驱动,用于连接DM6数据库 --><dependency><groupId>com.github.tianjing</groupId><artifactId>Dm6JdbcDriver</artifactId><version>1.0.0</version></dependency><!-- DM8 JDBC驱动,用于连接DM8数据库 --><dependency><groupId>com.dameng</groupId><artifactId>DmJdbcDriver18</artifactId><version>8.1.3.62</version></dependency><!-- Hutool工具类库,用于简化Java开发 --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.27</version></dependency>

2. 配置文件

spring.datasource:dmprimary:driver-class-name: dm6.jdbc.driver.DmDriver # 驱动类名称,用于连接 DM6 数据库jdbc-url: jdbc:dm6://localhost:12345/xxxx  # JDBC URL,指定 DM6 数据库的地址和端口username: xxxx  # 数据库用户名password: xxxxxxx  # 数据库密码connection-test-query: select 1  # 用于测试数据库连接的查询语句type: com.zaxxer.hikari.HikariDataSource  # 使用 HikariCP 作为连接池实现maximum-pool-size: 8  # 最大连接池大小minimum-idle: 2  # 最小空闲连接数idle-timeout: 600000  # 空闲连接的超时时间,单位毫秒max-lifetime: 1800000  # 连接的最大生命周期,单位毫秒connection-timeout: 3000  # 获取连接的超时时间,单位毫秒validation-timeout: 3000  # 验证连接的超时时间,单位毫秒initialization-fail-timeout: 1  # 初始化失败时的超时时间,单位毫秒leak-detection-threshold: 0  # 连接泄漏检测的阈值,单位毫秒dmbackup:driver-class-name: dm6.jdbc.driver.DmDriverjdbc-url: jdbc:dm6://8.8.8.8:12345/xxxxusername: xxxxxxxpassword: xxxxxconnection-test-query: select 1type: com.zaxxer.hikari.HikariDataSourcemaximum-pool-size: 8minimum-idle: 2idle-timeout: 600000max-lifetime: 1800000connection-timeout: 30000validation-timeout: 5000initialization-fail-timeout: 1leak-detection-threshold: 0dm7:driver-class-name: dm.jdbc.driver.DmDriverjdbc-url: jdbc:dm://localhost:5236/xxxxpassword: xxxxxxxxxusername: xxxxxxconnection-test-query: select 1type: com.zaxxer.hikari.HikariDataSourcemaximum-pool-size: 10minimum-idle: 2idle-timeout: 600000max-lifetime: 1800000connection-timeout: 30000validation-timeout: 5000initialization-fail-timeout: 1leak-detection-threshold: 0mybatis:mapper-locations: classpath:/mappers/*.xml  # 修改为你的 MyBatis XML 映射文件路径configuration:#    log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true

3. 定义数据源相关的常量

/*** 定义数据源相关的常量* @Author: 阿水* @Date: 2024-05-24*/
public interface DataSourceConstant {String DB_NAME_DM6 = "dm";String DB_NAME_DM6_BACKUP = "dmBackup";String DB_NAME_DM7 = "dm7";
}

4. 创建自定义注解

 
import java.lang.annotation.*;
/*** 数据源切换注解* @Author: 阿水* @Date: 2024-05-24*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {String value() default DataSourceConstant.DB_NAME_DM6;}

5. 动态数据源类

/*** 动态数据源类* @Author: 阿水* @Date: 2024-05-24*/
public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DataSourceUtil.getDB();}
}

动态数据源切换的核心实现

在多数据源配置中,我们需要一个类来动态决定当前使用的数据源,这就是 DynamicDataSource 类。它继承自 Spring 提供的 AbstractRoutingDataSource,通过覆盖 determineCurrentLookupKey 方法,从 ThreadLocal 中获取当前数据源的标识符,并返回该标识符以决定要使用的数据源。

6. 数据源工具类

/*** 数据源工具类* @Author: 阿水* @Date: 2024-05-24*/
public class DataSourceUtil {/***  数据源属于一个公共的资源*  采用ThreadLocal可以保证在多线程情况下线程隔离*/private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();/*** 设置数据源名* @param dbType*/public static void setDB(String dbType) {contextHolder.set(dbType);}/*** 获取数据源名* @return*/public static String getDB() {return (contextHolder.get());}/*** 清除数据源名*/public static void clearDB() {contextHolder.remove();}
}

7. 数据源配置类


/*** 数据源配置类,用于配置多个数据源,并设置动态数据源。* @Author: 阿水* @Date: 2024-05-24*/
@Configuration
public class DataSourceConfig {@Bean(name = "primaryDataSource")@ConfigurationProperties(prefix = "spring.datasource.dmprimary")public DataSource primaryDataSource() {return DataSourceBuilder.create().build();}@Bean(name = "backupDataSource")@ConfigurationProperties(prefix = "spring.datasource.dmbackup")public DataSource backupDataSource() {return DataSourceBuilder.create().build();}@Bean(name = "dm7")@ConfigurationProperties(prefix = "spring.datasource.dm7")public DataSource dataSourceDm7() {return DataSourceBuilder.create().build();}/*** 配置动态数据源,将多个数据源加入到动态数据源中* 设置 primaryDataSource 为默认数据源*/@Primary@Bean(name = "dynamicDataSource")public DataSource dynamicDataSource() {DynamicDataSource dynamicDataSource = new DynamicDataSource();dynamicDataSource.setDefaultTargetDataSource(primaryDataSource());Map<Object, Object> dsMap = new HashMap<>();dsMap.put(DataSourceConstant.DB_NAME_DM6, primaryDataSource());dsMap.put(DataSourceConstant.DB_NAME_DM6_BACKUP, backupDataSource());dsMap.put(DataSourceConstant.DB_NAME_DM7, dataSourceDm7());dynamicDataSource.setTargetDataSources(dsMap);return dynamicDataSource;}/*** 配置事务管理器,使用动态数据源*/@Beanpublic PlatformTransactionManager transactionManager() {return new DataSourceTransactionManager(dynamicDataSource());}
}

8. 数据源切换器

 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/*** 数据源切换器* @Author: 阿水* @Date: 2024-05-24*/@Configuration
public class DataSourceSwitcher extends AbstractRoutingDataSource {@Autowiredprivate DataSource primaryDataSource;@Autowiredprivate DataSource backupDataSource;private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();@PostConstructpublic void init() {this.setDefaultTargetDataSource(primaryDataSource);Map<Object, Object> dataSourceMap = new HashMap<>();dataSourceMap.put("primary", primaryDataSource);dataSourceMap.put("backup", backupDataSource);this.setTargetDataSources(dataSourceMap);this.afterPropertiesSet();}@Overrideprotected Object determineCurrentLookupKey() {return CONTEXT_HOLDER.get();}public static void setDataSource(String dataSource) {CONTEXT_HOLDER.set(dataSource);}public static void clearDataSource() {CONTEXT_HOLDER.remove();}public boolean isPrimaryDataSourceAvailable() {return isDataSourceAvailable(primaryDataSource);}public boolean isBackupDataSourceAvailable() {return isDataSourceAvailable(backupDataSource);}private boolean isDataSourceAvailable(DataSource dataSource) {try (Connection connection = dataSource.getConnection()) {return true;} catch (RuntimeException | SQLException e) {return false;}}
}

这个类通过继承 AbstractRoutingDataSource 实现了动态数据源切换的功能。它使用 ThreadLocal 变量实现线程隔离的数据源标识存储,并提供了设置和清除当前数据源的方法。在 Bean 初始化时,它将主数据源设为默认数据源,并将主数据源和备用数据源添加到数据源映射中。该类还提供了检查数据源可用性的方法,通过尝试获取连接来判断数据源是否可用。

这个类是实现动态数据源切换的核心部分,配合 Spring AOP 可以实现基于注解的数据源切换逻辑,从而实现多数据源和主备数据库的切换功能。

9. AOP切面类


import cn.hutool.core.util.ObjUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.util.Objects;
/*** AOP切面* @Author: 阿水* @Date: 2024-05-24*/
@Aspect
@Component
@Slf4j
@EnableAspectJAutoProxy
public class DataSourceAspect {@Autowiredprivate DataSourceSwitcher dataSourceSwitcher;@Autowiredprivate TimeCacheConfig cacheConfig;@Pointcut("@annotation(com.lps.config.DataSource) || @within(com.lps.config.DataSource)")public void dataSourcePointCut() {}/*** AOP环绕通知,拦截标注有@DataSource注解的方法或类* @param point 连接点信息* @return 方法执行结果* @throws Throwable 异常信息*/@Around("dataSourcePointCut()")public Object around(ProceedingJoinPoint point) throws Throwable {// 获取需要切换的数据源DataSource dataSource = getDataSource(point);log.info("初始数据源为{}", dataSource != null ? dataSource.value() : "默认数据源");// 设置数据源if (dataSource != null) {DataSourceUtil.setDB(dataSource.value());}// 处理主数据源逻辑if (DataSourceUtil.getDB().equals(DataSourceConstant.DB_NAME_DM6)) {handlePrimaryDataSource();}// 获取当前数据源String currentDataSource = DataSourceUtil.getDB();log.info("最终数据源为{}", currentDataSource);try {// 执行被拦截的方法return point.proceed();} finally {// 清除数据源DataSourceUtil.clearDB();log.info("清除数据源");}}/*** 处理主数据库的数据源切换逻辑*/private void handlePrimaryDataSource() {// 检查缓存中是否有主数据库挂掉的标记if (ObjUtil.isNotEmpty(cacheConfig.timeCacheHc().get("dataSource", false))) {// 切换到备用数据源DataSourceUtil.setDB(DataSourceConstant.DB_NAME_DM6_BACKUP);log.info("切换到备用数据源");} else {// 检查主数据库状态并切换数据源checkAndSwitchDataSource();}}/*** 检查主数据库状态并在必要时切换到备用数据库*/private void checkAndSwitchDataSource() {try {// 检查主数据库是否可用if (dataSourceSwitcher.isPrimaryDataSourceAvailable()) {log.info("主数据源没有问题,一切正常");} else {// 主数据库不可用,更新缓存并切换到备用数据源cacheConfig.timeCacheHc().put("dataSource", "主数据库挂了,boom");log.info("主数据源存在问题,切换备用数据源");DataSourceUtil.setDB(DataSourceConstant.DB_NAME_DM6_BACKUP);}} catch (Exception e) {// 主数据库和备用数据库都不可用,抛出异常throw new RuntimeException("两个数据库都有问题 GG", e);}}/*** 获取需要切换的数据源* @param point 连接点信息* @return 数据源注解信息*/private DataSource getDataSource(ProceedingJoinPoint point) {MethodSignature signature = (MethodSignature) point.getSignature();DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);if (Objects.nonNull(dataSource)) {return dataSource;}return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);}
}

10. 缓存配置类

/*** 缓存配置类* @Author: 阿水* @Date: 2024-05-24*/
@Configuration
public class TimeCacheConfig {@Beanpublic TimedCache timeCacheHc() {return CacheUtil.newTimedCache(5 * 60 * 1000);}
}

定时缓存,对被缓存的对象定义一个过期时间,当对象超过过期时间会被清理。此缓存没有容量限制,对象只有在过期后才会被移除,详情可以翻阅hutool官方文档

超时-TimedCache

11. 运行结果:

我dmprimary的信息随便写的,可以发现可以自动切换到备用数据库。

12. 结论

通过以上步骤,本次在Spring Boot项目中实现了自定义注解来管理多数据源,并且在主数据库不可用时自动切换到备用数据库。为了提升效率,我们还使用了缓存来记住主数据库的状态,避免频繁的数据库状态检查。这种设计不仅提高了系统的可靠性和可维护性,还能保证在关键时刻系统能够稳定运行。

希望这篇博客能对你有所帮助,如果你有任何问题或建议,欢迎留言讨论。(有问题可以私聊看到就会回)

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

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

相关文章

【HTML】制作一个跟随鼠标的流畅线条引导页界面(可直接复制源码)

目录 前言 HTML部分 CSS部分 JS部分 效果图 总结 前言 无需多言&#xff0c;本文将详细介绍一段HTML代码&#xff0c;图中线条可跟随鼠标移动&#xff0c;具体内容如下&#xff1a; 开始 首先新建一个HTML的文本&#xff0c;文本名改为[index.html]&#xff0c;创建好后右…

【逻辑漏洞】验证码绕过

【逻辑漏洞】验证码绕过 参数操纵值提取和重用自动化和识别其他测试 参数操纵 随机应变的请求方法&#xff1a;清空发送验证码参数。尝试更改HTTP方法从POST到GET或其他动词&#xff0c;并更改数据格式&#xff0c;例如在表单数据和JSON之间切换。发送空验证码&#xff1a;提交…

第十一节 SpringBoot Starter 面试题

一、面试题 很多同学的简历都写着熟悉 SpringBoot&#xff0c; 而 Starter 的实现原理被当作的考题的的情况越来越多。 来源牛客网关于 starter 的一些面试题 情景一、路虎一面 情景二、蔚来 情景三、同花顺 Starter 频频出现&#xff0c;因此在面试准备时&#xff0c;这道题…

mdm 推送证书制作教程

第一步点击获取&#xff0c;点击以后会下载一个zip压缩包 解压以后&#xff1a;会得到四个文件&#xff0c;请务必保存好&#xff0c;待会需要使用 登录apple开发者官网 https://developer.apple.com/account/resources/certificates/list 点击添加证书 找到mdm csr 然后点击…

赶紧收藏!2024 年最常见 20道 Redis面试题(六)

上一篇地址&#xff1a;赶紧收藏&#xff01;2024 年最常见 20道 Redis面试题&#xff08;五&#xff09;-CSDN博客 十一、Redis集群之间是如何复制的&#xff1f; Redis 集群是一个分布式系统&#xff0c;它由多个节点组成&#xff0c;这些节点共同存储数据并提供服务。在 R…

C++小游戏 合集

生化危机 #include<conio.h> #include<string.h> #include<stdio.h> #include<stdlib.h> #include<windows.h> #include<time.h> #include<direct.h> int n,round,gold0; bool f1,f2,f3,deadfalse,PC_64Bit; char str[4]; struct n…

用Python代码批量提取PDF文件中的表格

PDF文档中常常包含大量数据&#xff0c;尤其是官方报告、学术论文、财务报表等文档&#xff0c;往往包含了结构化的表格数据。表格作为承载关键信息的载体&#xff0c;其内容的准确提取对于数据分析、研究论证乃至业务决策具有重大意义。然而&#xff0c;PDF格式虽保证了文档的…

Vue从入门到实战Day12

一、Pinia快速入门 1. 什么是Pinia Pinia是Vue的最新状态管理工具&#xff0c;是Vuex的替代品 1. 提供更加简单的API&#xff08;去掉了mutation&#xff09; 2. 提供符合组合式风格的API&#xff08;和Vue3新语法统一&#xff09; 3. 去掉了modules的概念&#xff0c;每一…

C++|设计模式(二)|简单工厂和工厂方法模式

本文探讨两种广泛使用的创建型模式——简单工厂模式和工厂方法模式&#xff0c;解释他们的实现细节、优势以及应用场景。 在下一篇文章中&#xff0c;我会补充抽象工厂模式&#xff0c;其实工厂模式主要就是为了封装对象的创建过程&#xff0c;如果我们不进行封装&#xff0c;…

人工智能应用-实验7-胶囊网络分类minst手写数据集

文章目录 &#x1f9e1;&#x1f9e1;实验内容&#x1f9e1;&#x1f9e1;&#x1f9e1;&#x1f9e1;代码&#x1f9e1;&#x1f9e1;&#x1f9e1;&#x1f9e1;分析结果&#x1f9e1;&#x1f9e1;&#x1f9e1;&#x1f9e1;实验总结&#x1f9e1;&#x1f9e1; &#x1f9…

Python TCP编程简单实例

客户端&#xff1a;创建TCP链接时&#xff0c;主动发起连接的叫做客户端 服务端&#xff1a;接收客户端的连接 连接其他服务器 可以通过tcp连接其他服务器。 示例&#xff1a; import socket# 1.创建一个socket # 参数1&#xff1a;指定协议 AF_INET&#xff08;ipv4&#…

LeetCode1466重新规划路线

题目描述 n 座城市&#xff0c;从 0 到 n-1 编号&#xff0c;其间共有 n-1 条路线。因此&#xff0c;要想在两座不同城市之间旅行只有唯一一条路线可供选择&#xff08;路线网形成一颗树&#xff09;。去年&#xff0c;交通运输部决定重新规划路线&#xff0c;以改变交通拥堵的…

vite项目怎么build打包成不同环境的代码?从而适配不同环境api接口

在开发 Web 应用的过程中&#xff0c;我们需要在不同的环境中运行和测试我们的应用程序&#xff08;如开发环境、测试环境和生产环境&#xff09;。每个环境都有其特定的 API 接口和配置。Vite&#xff0c;一个基于 ESBuild 的前端构建工具&#xff0c;可以帮助我们实现这个需求…

判断当前系统是linux、windows还是MacOS (python)

在很多情况下&#xff0c;需要在python中获取当前系统的类型&#xff0c;用于判断是unix/windows/mac或者java虚拟机等&#xff0c;python中提供了os.name&#xff0c; sys.platform&#xff0c; platform.system等方式 sys sys.platform会返回当前系统平台的标识符&#xff…

Linux系统——面试题分享

目录 1.现在给你三百台服务器&#xff0c;你怎么对他们进行管理&#xff1f; 2.简述 raid0 raid1 raid5 三种工作模式的工作原理及特点 2.1RAID 0 ——可以是一块盘和 N 个盘组合 2.2RAID 1 ——只能2块盘&#xff0c;盘的大小可以不一样&#xff0c;以小的为准 2.3RAID 5 …

ganglia的安装使用

1.集群内分别安装epel-release依赖&#xff0c;更新yum源 sudo yum -y install epel-release 2&#xff0e;各节点上分别安装gmond sudo yum -y install ganglia-gmond 3.监控节点上安装gmetad和web(这里安装在node1上) sudo yum -y install ganglia-gmetad sudo yum -y insta…

现代密码学——消息认证和哈希函数

1.概述 1.加密-->被动攻击&#xff08;获取消息内容、业务流分析&#xff09; 消息认证和数字签名-->主动攻击&#xff08;假冒、重放、篡改、业务拒绝&#xff09; 2.消息认证作用&#xff1a; 验证消息源的真实性&#xff0c; 消息的完整性&#xff08;未被篡改…

Docker基本操作命令

Docker 是一个开源的应用容器引擎&#xff0c;允许开发者打包他们的应用以及应用的依赖包到一个可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器或者 Windows 服务器上。这使得应用可以在几乎任何地方以相同的方式运行。今天&#xff0c;我们将详细探讨一些基本的 …

第七步 实现打印函数

文章目录 前言一、如何设计我们的打印函数&#xff1f;二、实践检验&#xff01; 查看系列文章点这里&#xff1a; 操作系统真象还原 前言 现在接力棒意见交到内核手中啦&#xff0c;只不过我们的内核现在可谓是一穷二白啥都没有&#xff0c;为了让我们设计的内核能被看见被使用…