在现代的企业应用开发中,使用多数据源是一个常见的需求。尤其在关键应用中,设置主备数据库可以提高系统的可靠性和可用性。在这篇博客中,我将展示如何在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项目中实现了自定义注解来管理多数据源,并且在主数据库不可用时自动切换到备用数据库。为了提升效率,我们还使用了缓存来记住主数据库的状态,避免频繁的数据库状态检查。这种设计不仅提高了系统的可靠性和可维护性,还能保证在关键时刻系统能够稳定运行。
希望这篇博客能对你有所帮助,如果你有任何问题或建议,欢迎留言讨论。(有问题可以私聊看到就会回)