Spring Boot中如何优雅实现多数据源动态切换方案

Spring Boot 多数据源动态切换方案:一场优雅的舞会

大家好!今天我们来聊聊Spring Boot中多数据源动态切换这个话题。在实际的业务场景中,我们经常会遇到需要连接多个数据库的情况。比如,分库分表、读写分离、以及需要访问不同类型数据库等等。如何优雅地实现多数据源的动态切换,避免代码的冗余和维护的困难,是我们需要解决的问题。

我们的目标是:

  1. 易于配置: 方便地添加、删除和修改数据源。
  2. 动态切换: 在运行时根据需要切换到不同的数据源。
  3. 低侵入性: 尽可能少地修改现有代码。
  4. 高可维护性: 代码结构清晰,易于理解和维护。

接下来,我们将从以下几个方面展开讨论:

  1. 方案选择: 比较几种常见的多数据源方案的优缺点。
  2. 核心实现: 详细讲解基于AbstractRoutingDataSource的动态数据源方案。
  3. 配置与使用: 如何配置数据源、定义数据源上下文、以及在代码中切换数据源。
  4. AOP 拦截: 使用AOP拦截器自动切换数据源。
  5. 事务管理: 多数据源环境下的事务管理。
  6. 一些值得注意的地方: 讨论一些常见的问题和注意事项。

1. 方案选择:条条大路通罗马,选哪条?

在Spring Boot中,实现多数据源的方式有很多种,常见的包括:

  • 使用多个DataSource Bean: 最简单直接的方式,为每个数据源配置一个DataSource Bean。需要在代码中显式地指定使用哪个DataSource,比较繁琐,且不易维护。
  • 基于AbstractRoutingDataSource的动态数据源: Spring提供的抽象类,通过重写determineCurrentLookupKey()方法来动态决定使用哪个数据源。 这是我们今天重点讲解的方案。
  • ShardingSphere等中间件: 功能强大,支持分库分表、读写分离等复杂场景。但引入了额外的依赖,增加了系统的复杂性。
方案 优点 缺点 适用场景
多个DataSource Bean 简单直接 代码冗余,不易维护,需要在代码中显式指定DataSource 数据源数量较少,且切换逻辑简单的情况
AbstractRoutingDataSource 灵活,易于扩展,可以方便地实现动态切换 需要编写额外的代码,需要维护数据源上下文 适用于需要动态切换数据源,且切换逻辑不复杂的情况
ShardingSphere 功能强大,支持分库分表、读写分离等复杂场景,提供数据治理能力 引入额外的依赖,增加了系统的复杂性,学习成本较高,配置相对复杂 适用于需要分库分表、读写分离等复杂场景,且对数据治理有较高需求的情况

综合考虑,对于大多数中小型的项目,基于AbstractRoutingDataSource的方案已经足够满足需求,并且具有良好的灵活性和可维护性。

2. 核心实现:AbstractRoutingDataSource,动态切换的发动机

AbstractRoutingDataSource是Spring提供的一个抽象类,它实现了DataSource接口,并提供了一个determineCurrentLookupKey()方法,用于决定当前应该使用哪个数据源。

我们的核心思路是:

  1. 创建一个继承自AbstractRoutingDataSource的类,比如DynamicDataSource
  2. 重写determineCurrentLookupKey()方法,在这个方法中根据某种规则(比如线程变量、注解等)返回一个数据源的Key。
  3. 在Spring容器中配置多个DataSource Bean,并为每个DataSource指定一个Key。
  4. DynamicDataSource配置为Spring的DataSource Bean,并将所有的DataSource Bean注册到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.propertiesapplication.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);
    }
}

在这个配置类中,我们做了以下几件事:

  1. 配置了两个DataSource Bean:primaryDataSourcesecondaryDataSource
  2. 配置了DynamicDataSource Bean,并将primaryDataSourcesecondaryDataSource注册到DynamicDataSource中。
  3. 设置了DynamicDataSource的默认数据源为primaryDataSource
  4. 配置了事务管理器,这里需要注意,事务管理器需要使用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();
    }
}

在这个拦截器中,我们做了以下几件事:

  1. 定义了一个切点,用于拦截带有@TargetDataSource注解的方法。
  2. before方法中,获取@TargetDataSource注解的值,并设置当前的数据源Key。
  3. 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进行事务管理:

  1. 单一数据源事务: 如果你的业务逻辑只需要操作一个数据源,那么直接使用 @Transactional 注解即可,Spring会自动使用配置的 PlatformTransactionManager 来管理事务。

    @Service
    public class MyService {
    
        @Autowired
        private MyRepository myRepository;
    
        @Transactional
        public void updateData(Long id, String newData) {
            // 所有数据库操作都会在一个事务中
            myRepository.updateData(id, newData);
        }
    }
  2. 多数据源事务(同库): 如果你的业务逻辑需要操作多个数据源,但是它们都在同一个数据库实例中,那么仍然可以使用 @Transactional 注解,但是需要确保所有的数据源都使用同一个事务管理器。

  3. 多数据源分布式事务: 如果你的业务逻辑需要跨多个不同的数据库实例进行事务操作,那么就需要引入分布式事务管理器。常用的选择包括:

  • 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来记录日志,或者在 DataSourceContextHoldersetDataSourceKey方法中记录日志。

优雅的切换,让代码更清晰

总而言之,通过AbstractRoutingDataSource,结合ThreadLocal的数据源上下文,以及AOP的拦截器,我们可以实现一个优雅的多数据源动态切换方案。 这种方案具有良好的灵活性和可维护性,可以满足大多数中小型的项目的需求。 需要注意的是,多数据源环境下的事务管理是一个比较复杂的问题,需要根据实际情况选择合适的解决方案。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注