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