下面介绍如何在 SpringBoot 项目中实现 数据源 与 数据库动态切换。本文将详细解析核心原理、关键组件及代码实现步骤,通过示例代码、注释说明以及思维导图,帮助您快速掌握该技术方案。😊
一、原理概述
在多数据源应用场景中,不同的业务需要访问不同的数据库。利用 动态数据源 技术,可以在运行时根据业务需求切换数据源,避免硬编码数据源配置,提升系统扩展性。实现动态切换的核心思路是:
- 通过 AbstractRoutingDataSource 抽象类创建自定义数据源,并重写
determineCurrentLookupKey
方法获取当前数据源的标识。 - 使用 ThreadLocal 存储数据源标识,实现线程级别的数据源切换。
- 通过 AOP 或自定义注解,在业务方法调用前后切换数据源。
二、关键组件及工作流程
组件 | 功能 | 说明 |
---|---|---|
DataSourceContextHolder | 利用 ThreadLocal 存储当前数据源标识 | 保证同一线程内数据源的一致性 |
DynamicDataSource | 继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey 方法 | 根据 DataSourceContextHolder 返回数据源 key |
自定义注解 | 标记需要切换数据源的方法 | 配合 AOP 在方法执行前后进行数据源设置与清理 |
DataSourceAspect | AOP 切面,拦截带有自定义注解的方法,实现数据源切换 | 在方法调用前设置数据源,执行完毕后恢复默认数据源 |
工作流程图:
flowchart TD
A[<span style="color: red;">业务方法调用</span>]
B[<span style="color: red;">AOP 拦截</span>]
C[<span style="color: red;">设置数据源(ThreadLocal)</span>]
D[<span style="color: red;">调用 DynamicDataSource.determineCurrentLookupKey()</span>]
E[<span style="color: red;">选择对应数据源</span>]
F[<span style="color: red;">执行业务逻辑</span>]
G[<span style="color: red;">清除 ThreadLocal 数据</span>]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
说明:
- AOP 拦截:通过自定义注解在方法调用前,设置当前数据源标识;
- DynamicDataSource 根据 ThreadLocal 中存储的 key 返回对应的数据源;
- 业务方法执行完后,清除 ThreadLocal 信息,防止数据污染。
三、实战代码示例
1. DataSourceContextHolder
package com.example.datasource;
public class DataSourceContextHolder {
// 使用 ThreadLocal 存储数据源标识,保证线程安全
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
// 设置数据源标识
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
// 获取当前数据源标识
public static String getDataSourceKey() {
return contextHolder.get();
}
// 清除数据源标识
public static void clearDataSourceKey() {
contextHolder.remove();
}
}
代码解析:
- ThreadLocal 用于存储当前线程数据源标识,避免多线程数据冲突;
- 提供
set
、get
、clear
三个基本操作,方便在 AOP 中进行调用。
2. DynamicDataSource
package com.example.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
// 重写 determineCurrentLookupKey 方法,返回当前数据源标识
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceKey();
}
}
代码解析:
- 继承 AbstractRoutingDataSource,重写
determineCurrentLookupKey
方法,返回当前线程设置的数据源 key,从而动态选择数据源。
3. 自定义注解与 AOP 切面
自定义注解
package com.example.datasource;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
// 指定数据源的名称,如 "master"、"slave" 等
String value() default "master";
}
代码解析:
- @DataSource 注解用于标记需要切换数据源的方法或类,value 属性指定目标数据源名称。
AOP 切面
package com.example.datasource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Order(-1) // 保证在事务之前执行
@Component
public class DataSourceAspect {
@Around("@annotation(com.example.datasource.DataSource)")
public Object switchDataSource(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSource dataSource = method.getAnnotation(DataSource.class);
// 设置当前数据源,使用注解中的 value 值
DataSourceContextHolder.setDataSourceKey(dataSource.value());
try {
// 执行目标方法
return point.proceed();
} finally {
// 方法执行完毕后清除数据源设置,防止影响其他线程
DataSourceContextHolder.clearDataSourceKey();
}
}
}
代码解析:
- @Around 拦截带有 @DataSource 注解的方法;
- 在方法执行前调用
DataSourceContextHolder.setDataSourceKey
设置数据源; - 方法执行后,通过
clearDataSourceKey
清理 ThreadLocal 数据,保证后续调用不受影响。
四、Spring 配置
在 Spring 配置中,将多个数据源配置好,并将 DynamicDataSource 注入到数据源 Bean 中:
package com.example.config;
import com.example.datasource.DynamicDataSource;
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.*;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfig {
// 主数据源配置
@Bean
@Primary
@ConfigurationProperties("spring.datasource.master")
public DataSourceProperties masterDataSourceProperties() {
return new DataSourceProperties();
}
@Bean(name = "masterDataSource")
@Primary
public DataSource masterDataSource() {
return masterDataSourceProperties().initializeDataSourceBuilder().build();
}
// 从数据源配置
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSourceProperties slaveDataSourceProperties() {
return new DataSourceProperties();
}
@Bean(name = "slaveDataSource")
public DataSource slaveDataSource() {
return slaveDataSourceProperties().initializeDataSourceBuilder().build();
}
// 配置动态数据源
@Bean
public DataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
// 配置事务管理器,确保事务支持动态数据源
@Bean
public DataSourceTransactionManager transactionManager(DataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
代码解析:
- 分别配置主数据源(master)与从数据源(slave),利用
@ConfigurationProperties
自动注入配置; - 将两个数据源封装到 DynamicDataSource 中,并设置默认数据源为 master;
- 配置事务管理器,确保事务在动态数据源环境下正常运行。
五、总结
通过以上步骤,我们实现了在 SpringBoot 项目中对 数据源 与 数据库动态切换 的完整解决方案。关键优势包括:
- 灵活性:根据业务需求动态切换数据源,支持多种数据库配置;
- 线程安全:利用 ThreadLocal 确保同一线程内数据源一致;
- 便捷性:自定义注解与 AOP 切面让切换过程透明,业务代码无需额外处理。
希望本文通过详细的代码示例与流程图,能为您提供 实战指导,助力构建高性能、可扩展的数据访问层。💡