在同一个线程下数据源多次切换的回溯问题

原创 吴就业 91 0 2020-04-24

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/3b9c04e511e14132911aa9a1ed489a80

作者:吴就业
链接:https://wujiuye.com/article/3b9c04e511e14132911aa9a1ed489a80
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

多数据源开源项目:easymulti-datasource-spring-boot-starter

版本号:1.0.6-RELEASE

日期:2020/04/24

更新内容:解决在同一个线程下数据源多次切换的回溯问题

在某些场景下,我们可能需要多次切换数据源才能处理完同一个请求,也就是在一个线程上多次切换数据源。

比如:ServiceA.a调用ServiceB.bServiceB.b调用ServiceC.cServiceA.a使用从库,ServiceB.b使用主库,ServiceC.c又使用从库,因此,这一调用链路一共需要动态切换三次数据源。

数据源的切换我们都是使用AOP完成,在方法执行之前切换,从注解上获取到数据源的key,将其保持到ThreadLocal

当方法执行完成或异常时,需要从ThreadLocal中移除切换记录,否则可能会影响别的不显示声明切换数据源的地方获取到错误的数据源,并且我们也需要保证ThreadLocalremove方法被调用,这在多次切换数据源的情况下就会出问题。

当调用ServiceA.a时,切换到从库,方法执行到一半时由于需要调用ServiceB.b方法,此时数据源又被切换到了主库,也就是说ServiceB.b方法切面将ServiceA.a方法切面的数据源切换记录覆盖了。

ServiceB.b方法执行完成后,ServiceB.b方法切面调用ThreadLocalremove方法,将ServiceB.b方法切面的数据源切换记录移除,此时回到ServiceA.a方法继续往下执行时,由于ThreadLocal存储null, 如果配置了默认使用的数据源为主库,那么ServiceA.a方法后面的数据库操作就都在主库上操作了。

这一现象我们可以称为方法调用回溯导致的动态数据源切换故障。

使用切面实现动态切换数据源的方法如下:

public class EasyMutiDataSourceAspect {
/**
     * 切换数据源
     *
     * @param point 切点
     * @return
     * @throws Throwable
     */
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        EasyMutiDataSource ds = method.getAnnotation(EasyMutiDataSource.class);
        if (ds == null) {
            DataSourceContextHolder.setDataSource(null);
        } else {
            DataSourceContextHolder.setDataSource(ds.value());
        }
        try {
            return point.proceed();
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }
}

为解决这个问题,我想到的是使用栈这个数据结构存储动态数据源的切换记录。当调用ServiceA.a方法需要切换数据源时,将数据源的key push到栈顶,当在ServiceA.a方法中调用ServiceB.b方法时,切面切换数据源也将ServiceB.b方法需要切换的数据源的key push到栈顶。代码如下:

public final class DataSourceContextHolder {
   
   /**
     * 设置数据源
     *
     * @param multipleDataSource
     */
    public static void setDataSource(EasyMutiDataSource.MultipleDataSource multipleDataSource) {
        // 用于存储切换记录的栈
        DataSourceSwitchStack switchStack = multipleDataSourceThreadLocal.get();
        if (switchStack == null) {
            switchStack = new DataSourceSwitchStack();
            multipleDataSourceThreadLocal.set(switchStack);
        }
        // 将当前切换的数据源推送到栈顶,覆盖上次切换的数据源
        switchStack.push(multipleDataSource);
    }
}

ServiceB.b方法执行完成时,方法切面需要调用clearDataSource方法将切换的数据源的keyThreadLocal中移除,这时我们可以先从栈顶中移除一个元素,再判断栈是否为空,为空再将栈从ThreadLocal中移除。pop操作将ServiceB.b方法切面切换的数据源的key移除后,栈顶就是调用ServiceB.b方法之前使用的数据源。

public final class DataSourceContextHolder {
    
   /**
     * 清除数据源
     */
    public static void clearDataSource() {
        DataSourceSwitchStack switchStack = multipleDataSourceThreadLocal.get();
        if (switchStack == null) {
            return;
        }
        // 回退数据源切换
        switchStack.pop();
        // 栈空则表示所有切换都已经还原,可以remove了
        if (switchStack.size() == 0) {
            multipleDataSourceThreadLocal.remove();
        }
    }
}

只有所有切点都调用完clearDataSource方法之后,再将保持数据源切换记录的栈从ThreadLocal中移除。每个切点执行完成之后,调用clearDataSource方法将自身的切换记录从栈中移除,栈顶存储的就是前一个切点的切换记录,即回退数据源切换。这就可以解决同一个线程下数据源多次切换的回溯问题,使数据源切换正常。

存储切换记录的栈在easymulti-datasource的时候如下。

class DataSourceSwitchStack {

    private EasyMutiDataSource.MultipleDataSource[] stack;
    private int topIndex;
    private int leng = 2;

    public DataSourceSwitchStack() {
        stack = new EasyMutiDataSource.MultipleDataSource[leng];
        topIndex = -1;
    }

    public void push(EasyMutiDataSource.MultipleDataSource source) {
        if (topIndex + 1 == leng) {
            leng *= 2;
            stack = Arrays.copyOf(stack, leng);
        }
        this.stack[++topIndex] = source;
    }

    public EasyMutiDataSource.MultipleDataSource peek() {
        return stack[topIndex];
    }

    public EasyMutiDataSource.MultipleDataSource pop() {
        return stack[topIndex--];
    }

    public int size() {
        return topIndex + 1;
    }

}
#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

基准测试框架JMH快速上手

基准测试Benchmark是测量、评估软件性能指标的一种测试,对某个特定目标场景的某项性能指标进行定量的和可对比的测试。

如何获取泛型类的参数化类型?解密Java泛型

框架怎么知道这个`T`到底是什么类型呢?

这又是导致事务注解@Transactional不生效的一个原因

事务方法`A`调用事务方法`B`,当方法`B`抛出的异常被方法`A` `catch`后会发生什么?

深入理解类加载阶段之准备阶段

准备阶段是为类中定义的静态变量分配内存并设置初始化值的阶段,这里的初始值通常情况下指的是对应类型的零值,比如int类型的零值为0。

访问者模式在ASM框架中的使用

访问者模式的定义是:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。

为什么要使用Redis的多数据库呢?

`Redis`多数据库是我在`Redis`设计中最糟糕的决定,我希望在某种程度上,我们可以放弃多个数据库的支持,但我认为可能已经太晚了,因为有很多人在工作中使用这个特性。