Spring Boot 多数据源动态切换方案:一场优雅的舞会
大家好!今天我们来聊聊Spring Boot中多数据源动态切换这个话题。在实际的业务场景中,我们经常会遇到需要连接多个数据库的情况。比如,分库分表、读写分离、以及需要访问不同类型数据库等等。如何优雅地实现多数据源的动态切换,避免代码的冗余和维护的困难,是我们需要解决的问题。
我们的目标是:
- 易于配置: 方便地添加、删除和修改数据源。
- 动态切换: 在运行时根据需要切换到不同的数据源。
- 低侵入性: 尽可能少地修改现有代码。
- 高可维护性: 代码结构清晰,易于理解和维护。
接下来,我们将从以下几个方面展开讨论:
- 方案选择: 比较几种常见的多数据源方案的优缺点。
- 核心实现: 详细讲解基于
AbstractRoutingDataSource的动态数据源方案。 - 配置与使用: 如何配置数据源、定义数据源上下文、以及在代码中切换数据源。
- AOP 拦截: 使用AOP拦截器自动切换数据源。
- 事务管理: 多数据源环境下的事务管理。
- 一些值得注意的地方: 讨论一些常见的问题和注意事项。
1. 方案选择:条条大路通罗马,选哪条?
在Spring Boot中,实现多数据源的方式有很多种,常见的包括:
- 使用多个
DataSourceBean: 最简单直接的方式,为每个数据源配置一个DataSourceBean。需要在代码中显式地指定使用哪个DataSource,比较繁琐,且不易维护。 - 基于
AbstractRoutingDataSource的动态数据源: Spring提供的抽象类,通过重写determineCurrentLookupKey()方法来动态决定使用哪个数据源。 这是我们今天重点讲解的方案。 - ShardingSphere等中间件: 功能强大,支持分库分表、读写分离等复杂场景。但引入了额外的依赖,增加了系统的复杂性。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
多个DataSource Bean |
简单直接 | 代码冗余,不易维护,需要在代码中显式指定DataSource |
数据源数量较少,且切换逻辑简单的情况 |
AbstractRoutingDataSource |
灵活,易于扩展,可以方便地实现动态切换 | 需要编写额外的代码,需要维护数据源上下文 | 适用于需要动态切换数据源,且切换逻辑不复杂的情况 |
| ShardingSphere | 功能强大,支持分库分表、读写分离等复杂场景,提供数据治理能力 | 引入额外的依赖,增加了系统的复杂性,学习成本较高,配置相对复杂 | 适用于需要分库分表、读写分离等复杂场景,且对数据治理有较高需求的情况 |
综合考虑,对于大多数中小型的项目,基于AbstractRoutingDataSource的方案已经足够满足需求,并且具有良好的灵活性和可维护性。
2. 核心实现:AbstractRoutingDataSource,动态切换的发动机
AbstractRoutingDataSource是Spring提供的一个抽象类,它实现了DataSource接口,并提供了一个determineCurrentLookupKey()方法,用于决定当前应该使用哪个数据源。
我们的核心思路是:
- 创建一个继承自
AbstractRoutingDataSource的类,比如DynamicDataSource。 - 重写
determineCurrentLookupKey()方法,在这个方法中根据某种规则(比如线程变量、注解等)返回一个数据源的Key。 - 在Spring容器中配置多个
DataSourceBean,并为每个DataSource指定一个Key。 - 将
DynamicDataSource配置为Spring的DataSourceBean,并将所有的DataSourceBean注册到DynamicDataSource中。
下面是DynamicDataSource的示例代码:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceKey();
}
}
可以看到,DynamicDataSource的核心逻辑非常简单,就是从DataSourceContextHolder中获取当前的数据源Key。
3. 配置与使用:让数据源像棋子一样,随心而动
接下来,我们需要配置数据源、定义数据源上下文、以及在代码中切换数据源。
3.1 配置数据源
首先,我们需要在application.properties或application.yml文件中配置多个数据源。
spring:
datasource:
primary:
url: jdbc:mysql://localhost:3306/primary_db?serverTimezone=UTC&useSSL=false
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
secondary:
url: jdbc:mysql://localhost:3306/secondary_db?serverTimezone=UTC&useSSL=false
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
3.2 定义数据源上下文
DataSourceContextHolder用于存储当前线程的数据源Key。我们需要使用ThreadLocal来保证线程安全。
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceKey(String dataSourceKey) {
contextHolder.set(dataSourceKey);
}
public static String getDataSourceKey() {
return contextHolder.get();
}
public static void clearDataSourceKey() {
contextHolder.remove();
}
}
3.3 配置DynamicDataSource
我们需要创建一个配置类,用于配置DynamicDataSource和多个DataSource Bean。
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.primary")
public DataSourceProperties primaryDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
public DataSource primaryDataSource() {
return primaryDataSourceProperties().initializeDataSourceBuilder().build();
}
@Bean
@ConfigurationProperties("spring.datasource.secondary")
public DataSourceProperties secondaryDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DataSource secondaryDataSource() {
return secondaryDataSourceProperties().initializeDataSourceBuilder().build();
}
@Bean
public DynamicDataSource dynamicDataSource(@Qualifier("primaryDataSource") DataSource primaryDataSource,
@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("primary", primaryDataSource);
targetDataSources.put("secondary", secondaryDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(primaryDataSource); // 设置默认数据源
return dynamicDataSource;
}
@Bean
public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
在这个配置类中,我们做了以下几件事:
- 配置了两个
DataSourceBean:primaryDataSource和secondaryDataSource。 - 配置了
DynamicDataSourceBean,并将primaryDataSource和secondaryDataSource注册到DynamicDataSource中。 - 设置了
DynamicDataSource的默认数据源为primaryDataSource。 - 配置了事务管理器,这里需要注意,事务管理器需要使用
DynamicDataSource作为数据源。
3.4 使用DynamicDataSource
现在,我们可以在代码中使用DynamicDataSource了。
import org.springframework.stereotype.Service;
@Service
public class MyService {
public void doSomethingWithPrimary() {
DataSourceContextHolder.setDataSourceKey("primary");
// 使用primaryDataSource执行数据库操作
System.out.println("Using primary data source");
DataSourceContextHolder.clearDataSourceKey();
}
public void doSomethingWithSecondary() {
DataSourceContextHolder.setDataSourceKey("secondary");
// 使用secondaryDataSource执行数据库操作
System.out.println("Using secondary data source");
DataSourceContextHolder.clearDataSourceKey();
}
}
在这个例子中,我们通过DataSourceContextHolder.setDataSourceKey()方法来设置当前的数据源Key,然后在执行数据库操作之前,Spring会自动切换到对应的数据源。 最后需要调用DataSourceContextHolder.clearDataSourceKey() 来清除当前线程的数据源key,避免影响后续操作。
4. AOP 拦截:让切换更自动化
手动切换数据源虽然可行,但比较繁琐,并且容易出错。我们可以使用AOP拦截器来自动切换数据源,从而简化代码。
4.1 定义注解
首先,我们需要定义一个注解,用于标记需要切换数据源的方法。
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String value() default "primary";
}
4.2 定义AOP拦截器
接下来,我们需要定义一个AOP拦截器,用于拦截带有@TargetDataSource注解的方法,并在方法执行之前切换数据源。
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
public class DataSourceAspect {
@Pointcut("@annotation(TargetDataSource)")
public void dataSourcePointCut() {
}
@Before("dataSourcePointCut()")
public void before(JoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
TargetDataSource targetDataSource = method.getAnnotation(TargetDataSource.class);
if (targetDataSource != null) {
DataSourceContextHolder.setDataSourceKey(targetDataSource.value());
System.out.println("Switch data source to: " + targetDataSource.value());
}
}
@After("dataSourcePointCut()")
public void after(JoinPoint point) {
DataSourceContextHolder.clearDataSourceKey();
}
}
在这个拦截器中,我们做了以下几件事:
- 定义了一个切点,用于拦截带有
@TargetDataSource注解的方法。 - 在
before方法中,获取@TargetDataSource注解的值,并设置当前的数据源Key。 - 在
after方法中,清除当前的数据源Key。
4.3 使用AOP拦截器
现在,我们可以在代码中使用AOP拦截器了。
import org.springframework.stereotype.Service;
@Service
public class MyService {
@TargetDataSource("primary")
public void doSomethingWithPrimary() {
// 使用primaryDataSource执行数据库操作
System.out.println("Using primary data source");
}
@TargetDataSource("secondary")
public void doSomethingWithSecondary() {
// 使用secondaryDataSource执行数据库操作
System.out.println("Using secondary data source");
}
}
在这个例子中,我们使用@TargetDataSource注解来标记需要切换数据源的方法。当调用这些方法时,AOP拦截器会自动切换到对应的数据源。
5. 事务管理:多数据源下的责任划分
在多数据源环境下,事务管理是一个需要特别注意的问题。我们需要确保每个数据源的事务都能正确地提交或回滚。
在上面的DataSourceConfig类中,我们已经配置了事务管理器。但是,这个事务管理器只能管理一个数据源的事务。如果我们需要在多个数据源上执行事务操作,我们需要使用JTA(Java Transaction API)或者手动管理事务。
这里我们重点介绍使用Spring提供的 @Transactional注解配合PlatformTransactionManager进行事务管理:
-
单一数据源事务: 如果你的业务逻辑只需要操作一个数据源,那么直接使用
@Transactional注解即可,Spring会自动使用配置的PlatformTransactionManager来管理事务。@Service public class MyService { @Autowired private MyRepository myRepository; @Transactional public void updateData(Long id, String newData) { // 所有数据库操作都会在一个事务中 myRepository.updateData(id, newData); } } -
多数据源事务(同库): 如果你的业务逻辑需要操作多个数据源,但是它们都在同一个数据库实例中,那么仍然可以使用
@Transactional注解,但是需要确保所有的数据源都使用同一个事务管理器。 -
多数据源分布式事务: 如果你的业务逻辑需要跨多个不同的数据库实例进行事务操作,那么就需要引入分布式事务管理器。常用的选择包括:
- JTA (Java Transaction API): 这是一个标准的 Java EE API,用于管理分布式事务。你需要配置一个 JTA 事务管理器,例如 Atomikos 或 Bitronix。
- Seata: 一个开源的分布式事务解决方案,提供了高性能和易用性。
- XA事务: 资源管理器(数据库)支持XA协议,事务管理器协调多个数据库的事务。
由于配置和使用相对复杂,这里不再详细展开JTA和XA事务的配置,但需要明确,复杂的多数据源事务需要引入分布式事务解决方案。
6. 一些值得注意的地方:细节决定成败
在使用多数据源动态切换方案时,还有一些值得注意的地方:
- 默认数据源: 建议设置一个默认数据源,以避免在没有指定数据源的情况下出现异常。
- 数据源Key的命名规范: 建议使用统一的命名规范,比如使用数据源的名称作为Key。
- 线程安全: 确保
DataSourceContextHolder的线程安全。 - 连接池配置: 为每个数据源配置合适的连接池参数,比如最大连接数、最小空闲连接数等。
- 异常处理: 在切换数据源时,需要处理可能出现的异常,比如数据源不存在、连接失败等。
- 日志记录: 记录数据源切换的日志,方便排查问题。
| 注意事项 | 说明 |
|---|---|
| 默认数据源 | 建议设置一个默认数据源,以避免在没有指定数据源的情况下出现异常。 |
| 数据源Key命名规范 | 建议使用统一的命名规范,比如使用数据源的名称作为Key。 |
| 线程安全 | 确保DataSourceContextHolder的线程安全,使用ThreadLocal来存储数据源Key。 |
| 连接池配置 | 为每个数据源配置合适的连接池参数,比如最大连接数、最小空闲连接数等。 |
| 异常处理 | 在切换数据源时,需要处理可能出现的异常,比如数据源不存在、连接失败等。 |
| 日志记录 | 记录数据源切换的日志,方便排查问题。 可以使用AOP来记录日志,或者在 DataSourceContextHolder的setDataSourceKey方法中记录日志。 |
优雅的切换,让代码更清晰
总而言之,通过AbstractRoutingDataSource,结合ThreadLocal的数据源上下文,以及AOP的拦截器,我们可以实现一个优雅的多数据源动态切换方案。 这种方案具有良好的灵活性和可维护性,可以满足大多数中小型的项目的需求。 需要注意的是,多数据源环境下的事务管理是一个比较复杂的问题,需要根据实际情况选择合适的解决方案。