在现代的企业应用开发中,使用多数据源是一个常见的需求。尤其在关键应用中,设置主备数据库可以提高系统的可靠性和可用性。在这篇博客中,我将展示如何在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驱动,用于连接DM7/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. 运行结果:

接口方法上使用注解即可,举例:我这里每个方法都被注解 @DataSource 修饰

/**
* @author 19449
* @description 针对表【STUDENT】的数据库操作Mapper
* @createDate 2024-03-22 22:09:34
* @Entity com.lps.domain.Student
*/
@Mapper
//@DataSource()
public interface StudentMapper {
    @DataSource()
    Integer selectCountDm6();
    @DataSource(DataSourceConstant.DB_NAME_DM6_BACKUP)
    Integer selectCountDm6Backup();
    @DataSource(DataSourceConstant.DB_NAME_DM7)
    Integer selectCountDm7();
    @SuppressWarnings("MybatisXMapperMethodInspection")
    @DataSource()
    List<Student> selectList();

    @DataSource()
    void insert(Student record);
}

我dmprimary的信息随便写的,可以发现可以自动切换到备用数据库。 可以看到数据源切换毫无问题,主备数据源切换 毫无问题。

12. 结论

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

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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐