在Spring Boot项目中实现根据条件动态切换到不同的MySQL数据库进行查询,可以通过配置多数据源来实现。需要为每个数据库配置一个数据源,然后根据业务逻辑动态选择数据源进行操作。基于条件动态切换不同的MySQL数据库,可以使用AbstractRoutingDataSource
来配置多数据源,并结合ThreadLocal
来存储当前线程所需的数据源标识。
以下贴出的代码,都是最终版的代码,注意事项都在后面有说明。
一、Maven依赖
首先,在你的pom.xml
中确保包含以下依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
</dependencies>
二、配置application.yml
在application.yml
中配置多个数据源。
spring:
datasource:
dynamic:
# 指定默认使用default数据源配置
primary: default
datasource:
default:
url: jdbc:mysql://127.0.0.1:3306/dmp?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: "5"
min-idle: "5"
max-active: "20"
max-wait: "60000"
time-between-eviction-runs-millis: "60000"
min-evictable-idle-time-millis: "300000"
validation-query: SELECT 1 FROM DUAL
test-while-idle: "true"
test-on-borrow: "false"
test-on-return: "false"
pool-prepared-statements: "true"
max-pool-prepared-statement-per-connection-size: "20"
filters: stat,wall
connectionProperties: druid.stat.mergeSql
removeAbandoned: "true"
removeAbandonedTimeout: "1800"
csfa:
url: jdbc:mysql://127.0.0.1:3306/dmp_csfa?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid: ${spring.datasource.dynamic.datasource.default.druid}
如果想要共用链接池配置,可以用配置文件里面的引用变量方式
${spring.datasource.dynamic.datasource.default.druid}
三、配置多数据源
在DataSourceConfig
类中配置多个数据源。
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.rongxin.dmp.mdm.collect.enums.BusinessTabEnum;
@Configuration
public class DataSourceConfig {
@Bean(name = "defaultDataSource")
// 根据前缀去配置文件中找到对应的配置
@ConfigurationProperties("spring.datasource.dynamic.datasource.default")
public DataSource defaultDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean("csfaDataSource")
@ConfigurationProperties("spring.datasource.dynamic.datasource.csfa")
public DataSource csfaDataSource() {
return DruidDataSourceBuilder.create().build();
}
// 添加这个注解,标记为默认数据源
@Primary
@Bean(name = "dynamicDataSource")
public DynamicDataSource dynamicDataSource(
// 延迟加载
@Lazy @Qualifier("defaultDataSource") DataSource defaultDataSource,
@Lazy @Qualifier("csfaDataSource") DataSource csfaDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("cac", defaultDataSource);
targetDataSources.put("csfa", csfaDataSource);
// 将多数据源设置进去
dynamicDataSource.setTargetDataSources(targetDataSources);
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
return dynamicDataSource;
}
@Bean
public PlatformTransactionManager transactionManager(
@Qualifier("dynamicDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
注意:
如果上面没有标注
@Lazy
注解的话,启动可能会报错:Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'dynamicDataSource': Requested bean is currently in creation: Is there an unresolvable circular reference?
原因:
BeanCurrentlyInCreationException
表示在创建dynamicDataSource Bean
时发生了循环依赖。通常,这是由于Spring在初始化dynamicDataSource时,需要依赖其他数据源Bean(例如default和csfa),而这些数据源可能也在依赖dynamicDataSource。解决:在注入时使用
@Lazy
注解(推荐),即可避免。或者通过ObjectProvider
来延迟获取数据源,避免在Bean初始化期间立即创建依赖的Bean。@Bean(name = "dynamicDataSource") @Primary public DynamicDataSource dynamicDataSource(ObjectProvider<DataSource> defaultProvider, ObjectProvider<DataSource> csfaProvider) { DynamicDataSource dynamicDataSource = new DynamicDataSource(); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("cac", defaultProvider.getIfAvailable()); targetDataSources.put("csfa", csfaProvider.getIfAvailable()); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(defaultProvider.getIfAvailable()); return dynamicDataSource; }
如果上面没有标注
@Primary
注解的话,启动可能会报错:Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'javax.sql.DataSource' available: expected single matching bean but found 3: defaultDataSource,csfaDataSource,dynamicDataSource
原因:这个错误是因为在Spring容器中存在多个DataSource bean,而在某些地方(例如事务管理器或MyBatis)Spring无法确定应该使用哪个DataSource,导致抛出了
NoUniqueBeanDefinitionException
。解决:在使用
@Autowired
注入 DataSource 时明确指定要使用的 DataSource bean(如上述注入事务管理器时使用的@Qualifier("dynamicDataSource")
)。或者你也可以通过设置@Primary
注解来指定默认的 DataSource。
四、配置数据源切换
创建一个DynamicDataSource
类,通过继承AbstractRoutingDataSource
实现动态数据源切换。
import org.apache.commons.lang3.StringUtils;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import com.rongxin.dmp.mdm.collect.enums.BusinessTabEnum;
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
public static String getDataSourceKey() {
String key = CONTEXT_HOLDER.get();
// 如果等于空,则使用默认数据源key
return StringUtils.isNotBlank(key) ? key : "cac";
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSourceKey();
}
}
五、动态切换数据源
有三种可选的切换数据源方法
1. 手动切换
在需要切换数据源的地方,使用DynamicDataSource.setDataSourceKey("dbKey")
来指定使用哪个数据源。
import com.example.config.DynamicDataSource;
import com.example.mapper.MyMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MyService {
private final MyMapper myMapper;
public MyService(MyMapper myMapper) {
this.myMapper = myMapper;
}
@Transactional
public void performDbOperations(String dbKey) {
try {
// 切换数据源
DynamicDataSource.setDataSourceKey(dbKey);
// 执行数据库操作
myMapper.someDbOperation();
} finally {
DynamicDataSource.clearDataSourceKey();
}
}
}
这样,基于传入的dbKey
参数,可以在不同的数据库之间动态切换执行操作。确保在操作完之后,调用DynamicDataSource.clearDataSourceKey()
以防止线程污染。
2. 使用注解自动切换
a. 定义自定义注解
首先,定义一个注解用于指定方法使用的数据源。
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DSSwitch {
String value() default "cac";
}
该注解可以作用在类上,也可作用在方法上,默认值是
cac
使用的时候不指定值,
@DSSwitch
则使用默认的cac
,使用指定的值@DSSwitch("csfa")
b. 创建AOP切面
创建一个AOP切面拦截带有@DataSource注解的方法,并在方法执行前设置数据源。
@Aspect
// 保证在@Transactional之前执行
@Order(-1)
@Component
public class DataSourceAspect {
@Pointcut("@within(com.rongxin.dmp.mdm.common.anno.DSSwitch) || @annotation(com.rongxin.dmp.mdm.common.anno.DSSwitch)")
public void dataSourcePointCut() {}
@Before("dataSourcePointCut() && @annotation(ds)")
public void before(DSSwitch ds) {
DynamicDataSource.setDataSourceKey(ds.value());
}
@Before("dataSourcePointCut() && @within(ds)")
public void beforeClass(DSSwitch ds) {
DynamicDataSource.setDataSourceKey(ds.value());
}
// @Around("dataSourcePointCut()")
// public Object around(ProceedingJoinPoint point) throws Throwable {
// try {
// // 执行方法
// return point.proceed();
// } finally {
// // 清理上下文
// DynamicDataSource.clearDataSourceKey();
// }
// }
@After("dataSourcePointCut()")
public void afterSwitchDataSource() {
DynamicDataSource.clearDataSourceKey();
}
}
切入点
@Pointcut
说明:
@within(com.rongxin.dmp.mdm.common.anno.DSSwitch)
:这个条件匹配任何被 com.rongxin.dmp.mdm.common.anno.DSSwitch 注解的类。也就是说,如果一个类被标注了 @DSSwitch 注解,那么切点将会匹配这个类中的所有方法。
@annotation(com.rongxin.dmp.mdm.common.anno.DSSwitch)
:这个条件匹配任何被 com.rongxin.dmp.mdm.common.anno.DSSwitch 注解的单个方法。也就是说,如果一个方法被标注了 @DSSwitch 注解,那么切点将会匹配这个方法。
@Around
和@After
的区别:
@Around 可以在方法执行之前和执行之后分别进行其他逻辑操作(日志记录、性能监控等)。
@After 只能在方法执行完之后进行逻辑操作,只能进行有限的操作。
c. 使用注解指定数据源
在需要指定数据源的方法或类上,使用@DataSource
注解。
import com.example.annotation.DataSource;
import com.example.mapper.MyMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
// 指定使用 default 数据源
@DataSource("default")
@Service
public class MyService {
private final MyMapper myMapper;
public MyService(MyMapper myMapper) {
this.myMapper = myMapper;
}
// 指定使用 csfa 数据源
@DataSource("csfa")
@Transactional
public void performDbOperations() {
// 执行数据库操作
myMapper.someDbOperation();
}
}
上面标注的意思是,MyService
整个类所有的方法都使用default
数据源,而方法performDbOperations()
则使用csfa
数据源。
注意
如果AOP切面没有生效,以下是可能的解决方案:
没有启用AOP代理,在启动类加上注解可能会解决
@EnableAspectJAutoProxy
可能存在其他AOP代理或者拦截器在数据源切换时重新设置了数据源。
如果你的方法或者类被
@Transactional
注解修饰,Spring的事务管理
可能会在方法执行前就已经确定了数据源。由于事务是在拦截器或AOP代理层开始的,如果在方法执行时再切换数据源,事务上下文已经绑定在之前的数据源上,所以切换无效。
可以在事务开始前确定数据源,或者对于希望使用指定数据源的方法新启动一个事务(REQUIRES_NEW)。
设置注解AOP切面的执行顺序:在切面类上添加注解
@Order(-1)
即可。
3. 使用拦截器自动切换
a. 添加拦截器
使用 Spring 的拦截器来拦截每个请求,从请求头中提取 bus-tab
并设置到DynamicDataSource
中。
@Component
public class DataSourceInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String busTab = request.getHeader("bus-tab");
// 根据 bus-tab 设置数据源
DynamicDataSource.setDataSourceKey(busTab);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理数据源标识
DynamicDataSource.clearDataSourceKey();
}
}
b. 配置拦截器
确保拦截器在 Spring 的配置中生效。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private DataSourceInterceptor dataSourceInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(dataSourceInterceptor);
}
}
配置好拦截器之后,每次发送的请求中,请求头都会携带一个bus-tab
标识,根据标识可以动态切换数据源,不需要再去手动设置了,也不需要在类或方法上添加注解去动态切换。
当然,也可以把上面的三种切换方式组合使用,达到更加精细化的配置。
除了上面的配置动态多数据源切换,还能使用
Apache ShardingSphere
实现动态数据源的切换,有兴趣的可以去了解下。
评论区