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

350 阅读3分钟

作者开源的几个项目都有在项目中使用,并且已经发布到maven中央仓库,遇到问题会及时解决,欢迎大家使用,有问题可到github提issues。

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

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

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

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

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

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

当ServiceB.b方法执行完成后,ServiceB.b方法切面调用ThreadLocal的remove方法,将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方法将切换的数据源的key从ThreadLocal中移除,这时我们可以先从栈顶中移除一个元素,再判断栈是否为空,为空再将栈从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;
}

}