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

news2024/7/6 20:01:42

在现代的企业应用开发中,使用多数据源是一个常见的需求。尤其在关键应用中,设置主备数据库可以提高系统的可靠性和可用性。在这篇博客中,我将展示如何在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.DmDriver
    jdbc-url: jdbc:dm6://8.8.8.8:12345/xxxx
    username: xxxxxxx
    password: xxxxx
    connection-test-query: select 1
    type: com.zaxxer.hikari.HikariDataSource
    maximum-pool-size: 8
    minimum-idle: 2
    idle-timeout: 600000
    max-lifetime: 1800000
    connection-timeout: 30000
    validation-timeout: 5000
    initialization-fail-timeout: 1
    leak-detection-threshold: 0

  dm7:
    driver-class-name: dm.jdbc.driver.DmDriver
    jdbc-url: jdbc:dm://localhost:5236/xxxx
    password: xxxxxxxxx
    username: xxxxxx
    connection-test-query: select 1
    type: com.zaxxer.hikari.HikariDataSource
    maximum-pool-size: 10
    minimum-idle: 2
    idle-timeout: 600000
    max-lifetime: 1800000
    connection-timeout: 30000
    validation-timeout: 5000
    initialization-fail-timeout: 1
    leak-detection-threshold: 0

mybatis:
  mapper-locations: classpath:/mappers/*.xml  # 修改为你的 MyBatis XML 映射文件路径
  configuration:
    #    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-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 {
    @Override
    protected 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;
    }
    /**
     * 配置事务管理器,使用动态数据源
     */
    @Bean
    public 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 {

    @Autowired
    private DataSource primaryDataSource;

    @Autowired
    private DataSource backupDataSource;

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

    @PostConstruct
    public void init() {
        this.setDefaultTargetDataSource(primaryDataSource);
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("primary", primaryDataSource);
        dataSourceMap.put("backup", backupDataSource);
        this.setTargetDataSources(dataSourceMap);
        this.afterPropertiesSet();
    }

    @Override
    protected 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 {

    @Autowired
    private DataSourceSwitcher dataSourceSwitcher;

    @Autowired
    private 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 {
    @Bean
    public TimedCache timeCacheHc() {
        return CacheUtil.newTimedCache(5 * 60 * 1000);
    }
}

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

超时-TimedCache

11. 运行结果:

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

12. 结论

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

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

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

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

相关文章

VSCODE gcc运行多个.c文件

一、简介 很多时候&#xff0c;开发者需要使用VSCODE进行C语言算法验证。而VSCODE的gcc编译&#xff0c;默认是只编译本文件的内容&#xff0c;其他.c文件是不参与编译的。这就给开发者带来很大的困扰&#xff0c;因为开发者不可能把所有的算法都写在一个.c文件&#xff0c;特别…

arXiv AI 综述列表(2024.05.20~2024.05.24)

公众号&#xff1a;EDPJ&#xff08;进 Q 交流群&#xff1a;922230617 或加 VX&#xff1a;CV_EDPJ 进 V 交流群&#xff09; 每周末更新&#xff0c;完整版进群获取。 Q 群在群文件&#xff0c;VX 群每周末更新。 目录 1. Beyond Traditional Single Object Tracking: A …

在洁净实验室设计装修中怎么选择合适实验室家具?

在现代科学研究和技术开发中&#xff0c;洁净实验室装修设计成为了确保实验准确性和安全性的重要因素。洁净实验室需要提供一个无尘、无菌、受控的环境&#xff0c;而在洁净实验室装修设计这个过程中&#xff0c;如何选择合适的实验室家具就显得尤为重要&#xff0c;因为它直接…

【NumPy】NumPy实战入门:索引与切片(sort、argsort、searchsorted)详解

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

【静态分析】在springboot使用太阿(Tai-e)03

参考&#xff1a;使用太阿&#xff08;Tai-e&#xff09;进行静态代码安全分析&#xff08;spring-boot篇三&#xff09; - 先知社区 1. JavaApi 提取 1.1 分析 预期是提取controller提供的对外API&#xff0c;例如下图中的/sqli/jdbc/vuln 先看一下如何用tai-e去获取router…

AI+BI?国内期待值最高的4款智能问答类BI产品测评

AI大模型的这股风终是吹到了数据分析圈。与传统BI相比&#xff0c;问答BI进一步降低了数据获取门槛&#xff0c;通过对话的方式就可以访问数据并得出相应结论&#xff0c;更方便业务人员快速上手分析数据&#xff01; 问答BI&#xff08;Q&A BI&#xff09;在数据分析领域…

平板如何实现无纸化会议

为了实现高效的无纸化会议&#xff0c;连通宝可以是在内部网络部署&#xff0c;那么&#xff0c;平板如何实现无纸化会议&#xff1f; 1. 服务器配置&#xff1a; 部署专用无纸化会议系统服务器&#xff08;如rhub无纸化会议服务器&#xff09;至组织的内部网络中。确保该服务…

18.SpringCloud Gateway

简介 SpringCloud Gateway是spingcloud家族的产品&#xff0c;使用netty实现的高性能服务网关&#xff0c;用于替换netflix公司的zuul网关实现。 参考地址&#xff1a; https://spring.io/projects/spring-cloud 术语 工作原理 Route Predicate Factories GatewayFilte…

LeetCode刷题之HOT100之多数元素

2024/5/21 起床走到阳台&#xff0c;外面绵柔细雨&#xff0c;手探出去&#xff0c;似乎感受不到。刚到实验室&#xff0c;窗外声音放大&#xff0c;雨大了。昨天的两题任务中断了&#xff0c;由于下雨加晚上有课。这样似乎也好&#xff0c;不让我有一种被强迫的感觉&#xff0…

张量 t-product 积(matlab代码)

参考文献&#xff1a;Tensor Robust Principal Component Analysis with a New Tensor Nuclear Norm 首先是文章2.3节中 t-product 的定义&#xff1a; 块循环矩阵&#xff1a; 参考知乎博主的例子及代码&#xff1a;&#xff08;t-product与t-QR分解&#xff0c;另一篇傅里叶对…

Springboot 多环境切换 方法

准备工作 假设系统中有以下几个yml文件&#xff1a; application.ymlapplication-dev.ymlapplication-prode.ymlapplication-test.yml 方法一&#xff1a;在Active Profiles:输入dev 启动效果&#xff1a; 方法二&#xff1a;在Environment variables: 输入spring.profile…

外汇天眼:风险预警!以下平台监管牌照被撤销!

监管信息早知道&#xff01;外汇天眼将每周定期公布监管牌照状态发生变化的交易商&#xff0c;以供投资者参考&#xff0c;规避投资风险。如果平台天眼评分过高&#xff0c;建议投资者谨慎选择&#xff0c;因为在外汇天眼评分高不代表平台没问题&#xff01; 以下是监管牌照发生…

【简单介绍下7-Zip,什么是7-Zip?】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

ThingsBoard如何拆分前后端分离启动

后端启动 前端启动 注意事项 ThingsBoard是一个开源的物联网平台&#xff0c;它原本的设计就考虑到了现代Web应用的前后端分离架构。尽管其核心是一个后端服务&#xff0c;负责设备连接、数据处理和存储等&#xff0c;但其用户界面是作为单独的前端应用程序实现的&#xff0c…

8srping循环依赖

循环依赖 1.由同事抛的一个问题开始 最近项目组的一个同事遇到了一个问题&#xff0c;问我的意见&#xff0c;一下子引起的我的兴趣&#xff0c;因为这个问题我也是第一次遇到。平时自认为对spring循环依赖问题还是比较了解的&#xff0c;直到遇到这个和后面的几个问题后&…

超大Sql文件切分工具SQLDumpSplitter —— 筑梦之路

官网&#xff1a;PLB PLB - SQLSplitter 用于将大型MySQL转储拆分为可独立执行的小型SQL文件。 显示100%时并不是已经处理完了&#xff0c;而是才开始 优点 软件程序小巧&#xff0c;不需要安装&#xff0c;直接点击运行就可以最厉害的是SQLDumpSplitter可以自动将结构语句&…

探索LangGraph:如何创建一个既智能又可控的航空客服AI

这种设计既保持了用户控制权&#xff0c;又确保了对话流程的顺畅。但随着工具数量的增加&#xff0c;单一的图结构可能会变得过于复杂。我们将在下一节中解决这个问题。 第三部分的图将类似于下面的示意图&#xff1a; 状态定义 首先&#xff0c;定义图的状态。我们的状态和L…

HCIA第二天复习上

延长传输距离-------中继器&#xff08;放大器&#xff09;------物理层设备 可以延长5倍传输距离 增加网络节点数量 网络拓扑结构 1直线型拓扑 信息安全性差 网络延迟高传输速度慢 2环形拓扑 3星型拓扑 4网状型拓扑 传输效率高&#xff0c;…

RISC-V压缩指令扩展测试

概述 RISC-V定义了压缩指令扩展&#xff08;compressed instruction-set extension &#xff09;&#xff0c;命名为“C”扩展。压缩指令使用16位宽指令替换32位宽指令&#xff0c;从而减少代码量。这个C扩展可运用在RV32、RV64和RV128指令集上&#xff0c;通常使用“RVC”来表…

继承初级入门复习

注意&#xff1a;保护和私有在类中没有区别&#xff0c;但是在继承中有区别&#xff0c;private在继承的子类不可见&#xff0c;protect在继承的子类可见 记忆方法&#xff1a;先看基类的修饰符是private&#xff0c;那都是不可见的。如果不是&#xff0c;那就用继承的修饰和基…