Bootstrap

详细分析Java中DynamicDataSourceContextHolder动态数据源切换(附Demo)

前言

操作Java项目的时候,避免不了多数据源

对此怎么在一个项目中灵活切换是个问题

对于Java的相关知识推荐阅读:java框架 零基础从入门到精通的学习路线 附开源项目面经等(超全)

采用jpa的依赖包:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

配置对应的数据源:application.properties

# 主数据源配置
spring.datasource.primary.url=jdbc:mysql://localhost:3306/primarydb
spring.datasource.primary.username=username
spring.datasource.primary.password=password
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver

# 辅助数据源配置
spring.datasource.secondary.url=jdbc:mysql://localhost:3306/secondarydb
spring.datasource.secondary.username=username
spring.datasource.secondary.password=password
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver

创建数据源配置:

@Configuration
@EnableTransactionManagement
public class DataSourceConfig {

    @Primary
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean(name = "entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder, @Qualifier("primaryDataSource") DataSource dataSource) {
        return builder.dataSource(dataSource).packages("your.primary.entity.package")
                .persistenceUnit("primary").build();
    }

    @Bean(name = "secondaryEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(
            EntityManagerFactoryBuilder builder, @Qualifier("secondaryDataSource") DataSource dataSource) {
        return builder.dataSource(dataSource).packages("your.secondary.entity.package")
                .persistenceUnit("secondary").build();
    }

    @Primary
    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(
            @Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }

    @Bean(name = "secondaryTransactionManager")
    public PlatformTransactionManager secondaryTransactionManager(
            @Qualifier("secondaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

切换数据源,此处使用@Qualifier的注解

@Service
public class YourService {

    @Autowired
    @Qualifier("primaryDataSource")
    private DataSource primaryDataSource;

    @Autowired
    @Qualifier("secondaryDataSource")
    private DataSource secondaryDataSource;

    @Autowired
    private YourRepository yourRepository;

    @Transactional(transactionManager = "transactionManager")
    public void methodUsingPrimaryDataSource() {
        // 在这里使用主数据源
    }

    @Transactional(transactionManager = "secondaryTransactionManager")
    public void methodUsingSecondaryDataSource() {
        // 在这里使用辅助数据源
    }
}

1. 基本知识

DynamicDataSourceContextHolder 是一个用于动态数据源切换的工具类

利用 ThreadLocal 存储当前线程的数据源信息,通过栈的方式实现数据源的嵌套切换

其基本知识,主要通过分析源码进行讲解:

在这里插入图片描述

第一:属性:

LOOKUP_KEY_HOLDER:使用 ThreadLocal 保存一个 Deque(双端队列,通常用作栈)对象,用于存储当前线程的数据源名称,这里使用 Deque 是为了支持嵌套切换数据源,即在一个方法调用链中可以多次切换数据源,并且在方法调用结束后能恢复到上一个数据源

private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
    @Override
    protected Deque<String> initialValue() {
        return new ArrayDeque<>();
    }
};

第二:构造方法:(私有构造方法,防止外部实例化该类,因为它是一个工具类,所有方法都是静态方法)

private DynamicDataSourceContextHolder() {
}

第三:方法

  1. 获取当前线程的数据源
    peek() 方法返回当前线程栈顶的数据源名称,不移除数据
public static String peek() {
    return LOOKUP_KEY_HOLDER.get().peek();
}
  1. 设置当前线程的数据源
    push(String ds) 方法将数据源名称 ds 压入当前线程的栈中
    如果 ds 为空,则压入空字符串
public static String push(String ds) {
    String dataSourceStr = DsStrUtils.isEmpty(ds) ? "" : ds;
    LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
    return dataSourceStr;
}
  1. 清空当前线程的数据源
    poll() 方法移除当前线程栈顶的数据源名称
    如果栈为空,则移除 ThreadLocal 对象,避免内存泄漏
public static void poll() {
    Deque<String> deque = LOOKUP_KEY_HOLDER.get();
    deque.poll();
    if (deque.isEmpty()) {
        LOOKUP_KEY_HOLDER.remove();
    }
}
  1. 强制清空当前线程的所有数据源信息
    clear() 方法清除当前线程的所有数据源信息,强制移除 ThreadLocal 对象
public static void clear() {
    LOOKUP_KEY_HOLDER.remove();
}

2. Demo

引入相应的依赖

<!-- Spring Boot Starter Data JPA -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- HikariCP for DataSource pooling -->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
</dependency>

<!-- MySQL Connector -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

在 application.yml 或 application.properties 文件中配置多个数据源

例如,配置两个数据源 dataSource1 和 dataSource2:

spring:
  datasource:
    dynamic:
      primary: dataSource1
      datasource:
        dataSource1:
          url: jdbc:mysql://localhost:3306/db1
          username: root
          password: password
          driver-class-name: com.mysql.cj.jdbc.Driver
        dataSource2:
          url: jdbc:mysql://localhost:3306/db2
          username: root
          password: password
          driver-class-name: com.mysql.cj.jdbc.Driver
        dataSource3:
          url: jdbc:mysql://localhost:3306/db3
          username: root
          password: password
          driver-class-name: com.mysql.cj.jdbc.Driver

执行的响应的测试文件:

public class DynamicDataSourceDemo {
    public static void main(String[] args) {
        // 设置第一个数据源
        DynamicDataSourceContextHolder.push("dataSource1");
        System.out.println("当前数据源: " + DynamicDataSourceContextHolder.peek());

        // 调用 serviceA
        serviceA();

        // 清空所有数据源信息
        DynamicDataSourceContextHolder.clear();
    }

    public static void serviceA() {
        // 在 serviceA 中切换到 dataSource2
        DynamicDataSourceContextHolder.push("dataSource2");
        System.out.println("serviceA 中的数据源: " + DynamicDataSourceContextHolder.peek());

        // 调用 serviceB
        serviceB();

        // 恢复到上一个数据源
        DynamicDataSourceContextHolder.poll();
        System.out.println("serviceA 结束后数据源: " + DynamicDataSourceContextHolder.peek());
    }

    public static void serviceB() {
        // 在 serviceB 中切换到 dataSource3
        DynamicDataSourceContextHolder.push("dataSource3");
        System.out.println("serviceB 中的数据源: " + DynamicDataSourceContextHolder.peek());

        // 调用完成后恢复到上一个数据源
        DynamicDataSourceContextHolder.poll();
        System.out.println("serviceB 结束后数据源: " + DynamicDataSourceContextHolder.peek());
    }
}

根据Demo输出结果如下:

当前数据源: dataSource1
serviceA 中的数据源: dataSource2
serviceB 中的数据源: dataSource3
serviceB 结束后数据源: dataSource2
serviceA 结束后数据源: dataSource1

3. 实战

实战与Demo也差不多

Oracle的数据源配置:

在这里插入图片描述

执行对应的测试类:

import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;


@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = manongyanjiuseng.class)
public class projectTest {

    @Test
    public void testUsingTosDataSource() {
        // 设置第一个数据源
        DynamicDataSourceContextHolder.push("tos200");
        System.out.println("当前数据源: " + DynamicDataSourceContextHolder.peek());

        // 调用 serviceA
        serviceA();

        // 清空所有的数据源信息
        DynamicDataSourceContextHolder.clear();

    }

    public static void serviceA() {
        // 在 serviceA 中切换到 dataSource2
        DynamicDataSourceContextHolder.push("master");
        System.out.println("serviceA 中的数据源: " + DynamicDataSourceContextHolder.peek());

        // 调用 serviceB
        serviceB();

        // 恢复到上一个数据源
        DynamicDataSourceContextHolder.poll();
        System.out.println("serviceA 结束后数据源: " + DynamicDataSourceContextHolder.peek());
    }

    public static void serviceB() {
        // 在 serviceB 中切换到 dataSource3
        DynamicDataSourceContextHolder.push("tos211");
        System.out.println("serviceB 中的数据源: " + DynamicDataSourceContextHolder.peek());

        // 调用完成后恢复到上一个数据源
        DynamicDataSourceContextHolder.poll();
        System.out.println("serviceB 结束后数据源: " + DynamicDataSourceContextHolder.peek());
    }
}

最终截图如下:

在这里插入图片描述

;