SpringBoot + 动态数据源切换实战:基于AOP与ThreadLocal的优雅实现

动态数据源切换实战:基于AOP与ThreadLocal的优雅实现

一、应用场景

在多数据源架构中,我们常需要动态切换数据源。典型场景包括:

  1. 多租户系统:不同租户使用独立数据库
  2. 读写分离:写操作主库,读操作从库
  3. 分库分表:根据业务规则路由到不同数据库

本文将解析一个基于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

五、注意事项

  1. 线程安全
    务必在finally块中执行DynamicDataSourceContextHolder.poll()清理上下文

  2. 事务管理
    @Transactional注解的方法需要在最外层声明@DS,否则会导致切换失效

  3. 性能优化
    建议为不同数据源配置独立的连接池:

    @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;

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值