Spring Boot 多数据源配置与动态切换策略

Spring Boot 多数据源配置与动态切换策略:一场数据世界的华丽冒险

各位看官,咱们今天聊点硬核的,关于Spring Boot多数据源配置和动态切换的那些事儿。想象一下,你是一个管理着庞大帝国的君王,手握着各种各样的数据资源,分布在不同的城堡(数据库)里。有的城堡存储着用户的喜好,有的城堡记录着商品的库存,还有的城堡负责处理订单的流转。你如何才能有效地管理这些城堡,并且在需要的时候快速地访问它们呢?这就是多数据源配置和动态切换要解决的问题。

Spring Boot,作为你的忠实管家,提供了强大的工具,帮助你轻松驾驭这场数据世界的华丽冒险。让我们一起深入探索,揭开它的神秘面纱。

为什么要玩多数据源?

首先,咱们得搞清楚,为什么我们需要多个数据库?难道一个数据库不够用吗?当然不是,理由多种多样,就像你总需要好几双鞋来搭配不同的场合一样:

  • 业务隔离: 不同的业务模块可能需要使用不同的数据库,以实现数据隔离,提高系统的稳定性和安全性。例如,用户数据和交易数据分开存储,避免相互干扰。

  • 性能优化: 某些数据库可能更适合特定类型的操作。例如,使用MySQL存储结构化数据,使用MongoDB存储非结构化数据,充分发挥各自的优势。

  • 分库分表: 当单个数据库的数据量过大时,可以采用分库分表策略,将数据分散到多个数据库中,提高查询效率。

  • 历史遗留问题: 有些项目可能已经使用了多个数据库,为了保持兼容性,需要继续使用多数据源。

总之,多数据源可以帮助我们更好地组织和管理数据,提高系统的性能、稳定性和可扩展性。

多数据源配置:搭建城堡的基础

在Spring Boot中配置多数据源,就像为你的王国建造不同的城堡一样,需要精心设计和规划。

1. 引入依赖:

首先,我们需要引入相关的依赖,例如MySQL驱动:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

当然,如果你要使用其他的数据库,比如PostgreSQL,那就引入对应的依赖。

2. 配置数据源:

接下来,我们需要在application.propertiesapplication.yml文件中配置数据源的信息。假设我们有两个数据库,一个是primary,一个是secondary

# Primary Database
spring.datasource.primary.url=jdbc:mysql://localhost:3306/primary_db?serverTimezone=UTC&useSSL=false
spring.datasource.primary.username=root
spring.datasource.primary.password=password
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver

# Secondary Database
spring.datasource.secondary.url=jdbc:mysql://localhost:3306/secondary_db?serverTimezone=UTC&useSSL=false
spring.datasource.secondary.username=root
spring.datasource.secondary.password=password
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver

3. 创建数据源配置类:

现在,我们需要创建一个配置类,来告诉Spring Boot如何创建和管理这些数据源。

import com.zaxxer.hikari.HikariDataSource;
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.core.JdbcTemplate;
import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.primary")
    public DataSourceProperties primaryDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    public DataSource primaryDataSource(@Qualifier("primaryDataSourceProperties") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean
    public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    @ConfigurationProperties("spring.datasource.secondary")
    public DataSourceProperties secondaryDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    public DataSource secondaryDataSource(@Qualifier("secondaryDataSourceProperties") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean
    public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

这段代码做了什么呢?

  • @Configuration: 告诉Spring这是一个配置类。
  • @ConfigurationProperties("spring.datasource.primary"): 将application.properties中以spring.datasource.primary开头的配置项绑定到DataSourceProperties对象上。
  • @Primary: 标记primaryDataSource为默认的数据源。如果没有指定具体的数据源,Spring Boot会默认使用这个数据源。
  • @Qualifier: 用于消除歧义,当有多个相同类型的Bean时,可以使用@Qualifier来指定要注入的Bean。
  • DataSourceProperties 是 Spring Boot 提供的一个类,用于封装数据源的配置信息。
  • HikariDataSource 是一个高性能的 JDBC 连接池,Spring Boot 默认使用它作为数据源。
  • JdbcTemplate 是 Spring JDBC 提供的一个工具类,用于简化 JDBC 操作。

4. 使用数据源:

现在,我们就可以在代码中使用这两个数据源了。例如,我们可以创建一个Service类,分别使用primaryJdbcTemplatesecondaryJdbcTemplate来操作不同的数据库:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Autowired
    @Qualifier("primaryJdbcTemplate")
    private JdbcTemplate primaryJdbcTemplate;

    @Autowired
    @Qualifier("secondaryJdbcTemplate")
    private JdbcTemplate secondaryJdbcTemplate;

    public String getDataFromPrimary() {
        return primaryJdbcTemplate.queryForObject("SELECT data FROM primary_table WHERE id = 1", String.class);
    }

    public String getDataFromSecondary() {
        return secondaryJdbcTemplate.queryForObject("SELECT data FROM secondary_table WHERE id = 1", String.class);
    }
}

在这个例子中,我们使用@Qualifier注解来指定要注入的JdbcTemplate对象。getDataFromPrimary()方法使用primaryJdbcTemplate来查询primary_table表的数据,getDataFromSecondary()方法使用secondaryJdbcTemplate来查询secondary_table表的数据。

动态数据源切换:城堡的灵活调度

仅仅配置好多个数据源是不够的,我们还需要能够根据不同的业务场景,动态地切换数据源。就像君王需要根据不同的战况,调动不同的军队一样,我们需要能够根据不同的业务需求,选择不同的数据库。

1. 定义数据源上下文:

首先,我们需要定义一个数据源上下文,用于存储当前线程使用的数据源。

public class DataSourceContextHolder {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

    public static void clearDataSource() {
        contextHolder.remove();
    }
}

这个类使用了ThreadLocal来存储数据源的名称,保证每个线程都有自己的数据源。

2. 创建动态数据源:

接下来,我们需要创建一个动态数据源,用于根据上下文中的数据源名称,选择实际的数据源。

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

AbstractRoutingDataSource是Spring提供的一个抽象类,用于实现动态数据源。determineCurrentLookupKey()方法用于确定当前的数据源的Key,这里我们从DataSourceContextHolder中获取数据源的名称。

3. 配置动态数据源:

现在,我们需要在配置类中配置动态数据源。

import com.zaxxer.hikari.HikariDataSource;
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.core.JdbcTemplate;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.primary")
    public DataSourceProperties primaryDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    public DataSource primaryDataSource(@Qualifier("primaryDataSourceProperties") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean
    public DataSource secondaryDataSource(@Qualifier("secondaryDataSourceProperties") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.secondary")
    public DataSourceProperties secondaryDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    public AbstractRoutingDataSource routingDataSource(
            @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 JdbcTemplate jdbcTemplate(@Qualifier("routingDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

在这个配置类中,我们创建了一个routingDataSource Bean,它是一个DynamicDataSource对象。我们将primaryDataSourcesecondaryDataSource添加到targetDataSources中,并设置primaryDataSource为默认的数据源。

4. 使用动态数据源:

现在,我们就可以在代码中使用动态数据源了。例如,我们可以创建一个Service类,根据不同的业务逻辑,切换不同的数据源:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public String getDataFromPrimary() {
        DataSourceContextHolder.setDataSource("primary");
        try {
            return jdbcTemplate.queryForObject("SELECT data FROM primary_table WHERE id = 1", String.class);
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }

    public String getDataFromSecondary() {
        DataSourceContextHolder.setDataSource("secondary");
        try {
            return jdbcTemplate.queryForObject("SELECT data FROM secondary_table WHERE id = 1", String.class);
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }
}

在这个例子中,getDataFromPrimary()方法在执行查询之前,将数据源设置为primary,执行查询之后,清除数据源。getDataFromSecondary()方法类似,只是将数据源设置为secondary

5. AOP 拦截实现动态切换 (更优雅的方式):

上面的手动设置方式,在每个方法里都需要添加 DataSourceContextHolder.setDataSource()DataSourceContextHolder.clearDataSource(),显得比较冗余。我们可以使用 AOP 来拦截方法,自动切换数据源,让代码更简洁优雅。

首先,我们需要定义一个注解,用于标记需要切换数据源的方法:

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    String dataSource() default "primary"; // 默认使用 primary 数据源
}

然后,创建一个 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 joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        TargetDataSource targetDataSource = method.getAnnotation(TargetDataSource.class);
        if (targetDataSource != null) {
            DataSourceContextHolder.setDataSource(targetDataSource.dataSource());
            System.out.println("切换数据源为: " + targetDataSource.dataSource()); // 可以加日志方便调试
        }
    }

    @After("dataSourcePointCut()")
    public void after(JoinPoint joinPoint) {
        DataSourceContextHolder.clearDataSource();
        System.out.println("清除数据源"); // 可以加日志方便调试
    }
}

最后,在你的 Service 类中使用 @TargetDataSource 注解:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @TargetDataSource(dataSource = "primary")
    public String getDataFromPrimary() {
        return jdbcTemplate.queryForObject("SELECT data FROM primary_table WHERE id = 1", String.class);
    }

    @TargetDataSource(dataSource = "secondary")
    public String getDataFromSecondary() {
        return jdbcTemplate.queryForObject("SELECT data FROM secondary_table WHERE id = 1", String.class);
    }
}

现在,你只需要在方法上添加 @TargetDataSource 注解,就可以自动切换数据源了,是不是优雅多了?

总结:

总的来说,Spring Boot多数据源配置和动态切换是一个强大的工具,可以帮助我们更好地管理和使用数据。但是,它也带来了一些复杂性。我们需要 carefully design 我们的数据源策略,并确保数据源之间的切换是线程安全的。希望这篇文章能够帮助你理解Spring Boot多数据源配置和动态切换的原理和使用方法,让你在数据世界的冒险中更加游刃有余。

注意事项:

  • 事务管理: 在使用多数据源时,需要特别注意事务管理。如果需要在多个数据源上执行事务操作,可以使用分布式事务管理器,例如Atomikos或Bitronix。
  • 连接池: 选择合适的连接池,并根据实际情况调整连接池的参数,以提高系统的性能。
  • 异常处理: 在切换数据源时,需要注意异常处理,避免出现数据不一致的情况。

希望这篇文章能让你对 Spring Boot 的多数据源配置和动态切换有一个更深入的了解。记住,实践才是检验真理的唯一标准,动手尝试一下,你就能真正掌握它了!Happy coding!

发表回复

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