版本号:1.0.6-RELEASE
日期:2020/04/24
更新内容:解决在同一个线程下数据源屡次切换的回溯问题java
做者开源的几个项目都有在项目中使用,而且已经发布到maven
中央仓库,遇到问题会及时解决,欢迎你们使用,有问题可到github
提issues
。git
在某些场景下,咱们可能须要屡次切换数据源才能处理完同一个请求,也就是在一个线程上屡次切换数据源。github
好比:ServiceA.a
调用ServiceB.b
,ServiceB.b
调用ServiceC.c
。ServiceA.a
使用从库,ServiceB.b
使用主库,ServiceC.c
又使用从库,所以,这一调用链路一共须要动态切换三次数据源。spring
数据源的切换咱们都是使用AOP
完成,在方法执行以前切换,从注解上获取到数据源的key
,将其保持到ThreadLocal
。数据库
当方法执行完成或异常时,须要从ThreadLocal
中移除切换记录,不然可能会影响别的不显示声明切换数据源的地方获取到错误的数据源,而且咱们也须要保证ThreadLocal
的remove
方法被调用,这在屡次切换数据源的状况下就会出问题。数据结构
当调用ServiceA.a
时,切换到从库,方法执行到一半时因为须要调用ServiceB.b
方法,此时数据源又被切换到了主库,也就是说ServiceB.b
方法切面将ServiceA.a
方法切面的数据源切换记录覆盖了。maven
当ServiceB.b
方法执行完成后,ServiceB.b
方法切面调用ThreadLocal
的remove
方法,将ServiceB.b
方法切面的数据源切换记录移除,此时回到ServiceA.a
方法继续往下执行时,因为ThreadLocal
存储null
, 若是配置了默认使用的数据源为主库,那么ServiceA.a
方法后面的数据库操做就都在主库上操做了。spring-boot
这一现象咱们能够称为方法调用回溯致使的动态数据源切换故障。this
使用切面实现动态切换数据源的方法以下:spa
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;
}
}
复制代码