前言
操作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() {
}
第三:方法
- 获取当前线程的数据源
peek() 方法返回当前线程栈顶的数据源名称,不移除数据
public static String peek() {
return LOOKUP_KEY_HOLDER.get().peek();
}
- 设置当前线程的数据源
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;
}
- 清空当前线程的数据源
poll() 方法移除当前线程栈顶的数据源名称
如果栈为空,则移除 ThreadLocal 对象,避免内存泄漏
public static void poll() {
Deque<String> deque = LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
- 强制清空当前线程的所有数据源信息
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());
}
}
最终截图如下: