❝微服务架构中多数据源切换是个常见的需求。Spring Boot 提供了强大的支持来简化这一过程.
多数据源切换原理
多数据源切换的原理主要基于 Spring 的 AbstractRoutingDataSource
类。AbstractRoutingDataSource
类允许根据运行时上下文动态选择数据源。其核心在于实现 determineCurrentLookupKey
方法,该方法决定当前操作使用哪个数据源。AbstractRoutingDataSource
实现多数据源切换的原理:
1. 数据源映射
AbstractRoutingDataSource
内部维护了一个映射(Map),用于存储数据源标识(key)和对应的数据源实例(value)。这个映射允许根据数据源标识快速查找和获取对应的数据源。
2. 数据源标识的确定
AbstractRoutingDataSource
提供了一个抽象方法determineCurrentLookupKey()
,该方法用于确定当前需要使用的数据源标识。这个方法需要由子类实现,以返回当前线程或请求应该使用的数据源标识。
3. 数据源的选择与连接获取
当应用程序需要获取数据库连接时, AbstractRoutingDataSource
的getConnection()
方法会被调用。这个方法首先调用determineCurrentLookupKey()
方法来获取当前的数据源标识,然后根据这个标识从内部映射中查找对应的数据源。一旦找到了对应的数据源, AbstractRoutingDataSource
就会调用该数据源的getConnection()
方法来获取实际的数据库连接,并将这个连接返回给应用程序。
4. 数据源切换的实现
为了实现数据源的动态切换,通常会在子类中重写 determineCurrentLookupKey()
方法,并根据当前的上下文(如线程变量)来确定返回的数据源标识。此外,通常会使用 ThreadLocal
来存储每个线程的数据源标识,这样每个线程都可以独立地切换数据源而不会互相干扰。
实现步骤
1. 依赖
引入 MySQL 和 Druid 的依赖
<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>
2. 配置数据源
在 application.yml
文件中配置多个数据源。
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
datasource1:
url: jdbc:mysql://localhost:3306/master_db
username: root
password: password
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
datasource2:
url: jdbc:mysql://localhost:3306/slave_db
username: root
password: password
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
3. 创建数据源配置类
创建一个配置类来定义数据源 Bean。
@Configuration
public class DataSourceConfig {
@Bean
@ConditionalOnProperty(prefix = "spring.datasource.druid", name = "datasource1")
@ConfigurationProperties(prefix = "spring.datasource.druid.datasource1")
public DataSource dataSource1() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConditionalOnProperty(prefix = "spring.datasource.druid", name = "datasource2")
@ConfigurationProperties(prefix = "spring.datasource.druid.datasource2")
public DataSource dataSource2() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(dataSource1());
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("dataSource1", dataSource1());
targetDataSources.put("dataSource2", dataSource2());
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
}
4. 实现 AbstractRoutingDataSource
创建一个继承自 AbstractRoutingDataSource
的类,并实现 determineCurrentLookupKey
方法。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
5. 创建 DataSourceContextHolder
创建一个工具类来保存当前线程的数据源信息。
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSource(String dataSource) {
CONTEXT_HOLDER.set(dataSource);
}
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
6. AOP动态切换数据源
使用 AOP 在方法执行前后切换数据源。
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class DataSourceAspect {
// 方法或者类上的横切点
@Pointcut("@annotation(dataSource) || @within(dataSource)")
public void dataSourcePointcut(DataSource dataSource) {}
@Before("dataSourcePointcut(dataSource)")
public void switchDataSource(JoinPoint joinPoint, DataSource dataSource) {
// 从注解中获取数据源标识
String dataSourceKey = dataSource.value();
// 切换到指定的数据源
DataSourceContextHolder.setDataSourceType(dataSourceKey);
}
@AfterReturning(pointcut = "dataSourcePointcut(dataSource)", returning = "result")
public void restoreDataSource(JoinPoint joinPoint, DataSource dataSource, Object result) {
// 恢复默认数据源(可选)
DataSourceContextHolder.clearDataSourceType();
}
}
/**
* 自定义注解
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default "dataSource1";
}
7. 使用自定义注解
在需要切换数据源的方法上使用自定义注解。
@Service
public class UserService {
@Autowired
private UserRepository userRepo;
@DataSource("dataSource2")
public void queryAndUpdate(List<Integer> deptIds) {
List<User> userList = userRepo.findAll();
System.out.println(userList.size());
// 其他操作
}
}
注意
数据源切换的逻辑应该尽可能简单和高效,以避免对应用程序性能产生负面影响。 在切换数据源时,需要注意事务管理的问题,确保在同一个事务中只使用同一个数据源。
太强 ! SpringBoot中出入参增强的5种方法 : 加解密、脱敏、格式转换、时间时区处理
太强 ! SpringBoot中优化if-else语句的七种绝佳方法实战
提升编程效率的利器: Google Guava库之RateLimiter优雅限流
SpringBoot中大量数据导出方案:使用EasyExcel并行导出多个excel文件并压缩zip后下载
Elasticsearch揭秘:高效写入与精准检索的流程原理全解析