版本號: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;
}
}
複製代碼