动态数据源切换实战:基于AOP与ThreadLocal的优雅实现
一、应用场景
在多数据源架构中,我们常需要动态切换数据源。典型场景包括:
- 多租户系统:不同租户使用独立数据库
- 读写分离:写操作主库,读操作从库
- 分库分表:根据业务规则路由到不同数据库
本文将解析一个基于Spring AOP和ThreadLocal的动态数据源切换实现方案。
二、核心实现解析
1. 整体架构图
[HTTP请求]
|
v
[拦截器/AOP] --> [数据源上下文] --> [路由决策]
|
v
[执行SQL] --> [清理上下文]
2. 关键代码解析
(1)AOP切面实现(DataSourceAspect)
@Before("datasourcePointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
// 优先级1:检查方法/类上的@DS注解
DS dataSource = getClassAnnotation(joinPoint);
if (Objects.isNull(dataSource)) {
// 优先级2:线程上下文数据源
String localDS = DataSourceContextHolder.getDataSource();
if (StringUtils.isNotBlank(localDS)) {
DynamicDataSourceContextHolder.push(localDS);
}
// 优先级3:HTTP请求头
else if (WebUtil.getRequest() != null) {
String headerDS = request.getHeader("dataSourceName");
DynamicDataSourceContextHolder.push(headerDS);
}
}
}
执行顺序:
方法注解 > 类注解 > 线程上下文 > 请求头
(2)上下文保持(DataSourceContextHolder)
public class DataSourceContextHolder {
// 使用TTL解决线程池环境下的上下文传递
private static final ThreadLocal<String> DATA_SOURCE =
new TransmittableThreadLocal<>();
public static void setDataSource(String dataSource) {
DATA_SOURCE.set(dataSource);
}
}
三、使用示例
场景1:注解式切换
@Service
public class OrderService {
@DS("master") // 强制使用主库
@Transactional
public void createOrder(Order order) {
// 写操作...
}
@DS("slave") // 指定从库
public Order getOrder(Long id) {
return orderMapper.selectById(id);
}
}
场景2:编程式切换
public void multiTenantOperation(String tenantId) {
try {
// 手动设置数据源
DataSourceContextHolder.setDataSource("tenant_" + tenantId);
// 执行数据库操作
tenantService.processBusiness();
} finally {
DataSourceContextHolder.clear(); // 必须清理!
}
}
场景3:通过HTTP请求动态切换
# 使用curl测试
curl -H "dataSourceName: slave-01" http://api.example.com/query/data
四、配置说明
1. 必要依赖
<!-- 动态数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>
<!-- 上下文传递 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
2. 配置文件
sto:
datasource:
dynamic:
enable: true
dataSourceName: master # 默认数据源
datasources:
master:
url: jdbc:mysql://master-db:3306/db
username: root
password: 123456
slave-01:
url: jdbc:mysql://slave1-db:3306/db
username: read_user
password: 123456
五、注意事项
-
线程安全
务必在finally
块中执行DynamicDataSourceContextHolder.poll()
清理上下文 -
事务管理
@Transactional
注解的方法需要在最外层声明@DS
,否则会导致切换失效 -
性能优化
建议为不同数据源配置独立的连接池:@Bean @DS("slave") public DataSource slaveDataSource() { return DataSourceBuilder.create().build(); }
六、总结
本文实现的动态数据源方案具有以下优势:
特性 | 说明 |
---|---|
多级优先级控制 | 注解 > 编程式 > 请求头 |
异步线程支持 | 通过TTL实现上下文传递 |
低入侵性 | 只需添加注解即可切换数据源 |
灵活扩展 | 轻松添加新数据源配置 |
七、附录
DataSourceAspect.java (完整版)
package com.coder.framework.datasource.core.aop;
import cn.hutool.core.util.StrUtil;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.coder.framework.common.util.http.WebUtil;
import com.coder.framework.datasource.core.context.DataSourceContextHolder;
import com.coder.framework.datasource.core.props.DataSourceProperties;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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 java.util.Objects;
/**
* 动态数据源切面
*/
@Slf4j
@Aspect
public class DataSourceAspect {
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional) " +
"|| execution( * com.coder.module.*.service.*.*.*(..)) " +
"|| execution( * com.coder.module.*.dal.*.*.*(..))")
public void datasourcePointcut() {
}
/**
* 前置操作
*/
@Before("datasourcePointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
// 获取类上的数据源注解
DS dataSource = getClassAnnotation(joinPoint);
// 没有指定数据源
if (Objects.isNull(dataSource)) {
// 尝试从请求头获取
HttpServletRequest request = WebUtil.getRequest();
if (StringUtils.isNotBlank(DataSourceContextHolder.getDataSource())) {
DynamicDataSourceContextHolder.push(DataSourceContextHolder.getDataSource());
} else if (Objects.nonNull(request)) {
String dataSourceName = request.getHeader(DataSourceProperties.DATA_SOURCE_NAME);
if (StringUtils.isNotBlank(dataSourceName)) {
DynamicDataSourceContextHolder.push(dataSourceName);
}
}
}
if (StrUtil.isNotBlank(DynamicDataSourceContextHolder.peek())) {
// 获取当前执行数据源
log.info("执行SQL的数据源:{}", DynamicDataSourceContextHolder.peek());
}
}
/**
* 获取类、接口、注解
*/
private DS getClassAnnotation(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 获取方法上的数据源注解
DS methodAnnotation = methodSignature.getMethod().getAnnotation(DS.class);
if (Objects.nonNull(methodAnnotation)) {
return methodAnnotation;
}
// 获取目标类的类型
Class<?> targetClass = joinPoint.getTarget().getClass();
// 获取接口上的注解
DS dataSource = targetClass.getAnnotation(DS.class);
if (Objects.nonNull(dataSource)) {
return dataSource;
}
// 目标类上没有注解 =》 检查其实现的接口
for (Class<?> clazz : targetClass.getInterfaces()) {
dataSource = clazz.getAnnotation(DS.class);
if (Objects.nonNull(dataSource)) {
return dataSource;
}
}
// 检查其类所属的父类
Class<?> superClass = targetClass.getSuperclass();
while (Objects.nonNull(superClass) && superClass != Object.class) {
dataSource = superClass.getAnnotation(DS.class);
if (Objects.nonNull(dataSource)) {
return dataSource;
}
superClass = superClass.getSuperclass();
}
return null;
}
/**
* 后置操作
*/
@After("datasourcePointcut()")
public void afterAdvice() {
DynamicDataSourceContextHolder.poll();
}
}
DataSourceContextHolder.java
package com.coder.framework.datasource.core.context;
import com.alibaba.ttl.TransmittableThreadLocal;
/**
* 本地线程数据源(用于特地情况下,需要指定某个数据源执行)
*/
public class DataSourceContextHolder {
private static final ThreadLocal<String> DATA_SOURCE = new TransmittableThreadLocal<>();
/**
* 设置数据源
*
* @param dataSource 数据源名称
*/
public static void setDataSource(String dataSource) {
DATA_SOURCE.set(dataSource);
}
/**
* 获取当前线程数据源名
*/
public static String getDataSource() {
return DATA_SOURCE.get();
}
/**
* 清空本地线程变量
*/
public static void clear() {
DATA_SOURCE.remove();
}
}
DataSourceProperties.java
package com.coder.framework.datasource.core.props;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 动态数据源配置
*
* @author L.ty
*/
@ConfigurationProperties(prefix = DataSourceProperties.PREFIX)
@Data
public class DataSourceProperties {
public static final String PREFIX = "sto.datasource.dynamic";
/**
* 默认数据源名称
*/
public static final String DATA_SOURCE_NAME = "dataSourceName";
/**
* 是否开启动态数据源
*/
private Boolean enable = false;
/**
* 数据源名称:如 master or salve
*/
private String dataSourceName = DATA_SOURCE_NAME;
}