最近在重構專案中,需要相容多資料來源,故此實現下多資料來源事務。
這次重構專案中,為了支援後續龐大的資料量接入,更迭了資料庫,但是為了要相容老版本,也不能直接拿掉老的資料庫。所以就有了相容多資料來源的需求,尤其是要保證事務。
其實這個需求就是要實現分散式事務,但是我們的這個場景是在一個服務內,所以可以利用AOP來輕量的實現這個需求,若是多個服務的話,就需要實現一個管理器。
具體實現
用過spring的都知道,我們一般都是使用@Transactional
註解,但是這個註解在多資料來源下,只能支援指定的資料來源(不指定就是預設的)。
所以我們新建個自定義註解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiTransactional {
String[] values() default "";
}
複製程式碼
再定義一個切面:
/**
* 多資料來源事務
*
* @author 7le
*/
@Slf4j
@Aspect
@Order(-7)
@Component
public class MultiTransactionalAspect {
@Autowired
private ApplicationContext applicationContext;
@Around(value = "@annotation(multiTransactional)")
public Object around(ProceedingJoinPoint pjp, MultiTransactional multiTransactional) throws Throwable {
Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack = new Stack<>();
Stack<TransactionStatus> transactionStatusStack = new Stack<>();
try {
if (!openTransaction(dataSourceTransactionManagerStack, transactionStatusStack, multiTransactional)) {
return null;
}
Object ret = pjp.proceed();
commit(dataSourceTransactionManagerStack, transactionStatusStack);
return ret;
} catch (Throwable e) {
rollback(dataSourceTransactionManagerStack, transactionStatusStack);
log.error(String.format(
"MultiTransactionalAspect catch exception class: %s method: %s detail:", pjp.getTarget().getClass().getSimpleName(),
pjp.getSignature().getName()), e);
throw e;
}
}
private boolean openTransaction(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
Stack<TransactionStatus> transactionStatusStack, MultiTransactional multiTransactional) {
String[] transactionMangerNames = multiTransactional.values();
if (ArrayUtils.isEmpty(multiTransactional.values())) {
return false;
}
for (String beanName : transactionMangerNames) {
DataSourceTransactionManager dataSourceTransactionManager = (DataSourceTransactionManager) applicationContext
.getBean(beanName);
TransactionStatus transactionStatus = dataSourceTransactionManager
.getTransaction(new DefaultTransactionDefinition());
transactionStatusStack.push(transactionStatus);
dataSourceTransactionManagerStack.push(dataSourceTransactionManager);
}
return true;
}
private void commit(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
Stack<TransactionStatus> transactionStatusStack) {
while (!dataSourceTransactionManagerStack.isEmpty()) {
dataSourceTransactionManagerStack.pop().commit(transactionStatusStack.pop());
}
}
private void rollback(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
Stack<TransactionStatus> transactionStatusStack) {
while (!dataSourceTransactionManagerStack.isEmpty()) {
dataSourceTransactionManagerStack.pop().rollback(transactionStatusStack.pop());
}
}
}
複製程式碼
這樣就大功告成,只需要在需要的方法上,加上@MultiTransactional({"xxxx","xxxxx"})
實現的程式碼在 springcloud-gateway
注意事項
在使用的時候,需要注意一些細節,要加上@EnableTransactionManagement
。
以及@MultiTransactional({"xxxx","xxxxx"})
是AOP的方式,那就意味著是動態代理的,那下面的方式就會失效:
- private, protected方法無效 (Spring AOP使用JDK動態代理或者CGLIB來為目標物件建立代理。使用原生的Java的代理是無法代理protected和private型別的方法。CGLIB的代理雖然在技術上可以代理protected和private型別的方法,但是用於AOP的時候不推薦代理protected和private型別的方法.)
- 同一個class中public方法無效 (@Transactional的事務開啟 ,是基於介面或者是類的代理被建立。所以在同一個類中一個無事務的方法呼叫另一個有事務的方法,將是對this引用進行呼叫而非代理,事務是不會起作用的。 )
- 註解寫在父類抽象方法上
隱患
上面也提到過這個方案比較輕量,也是針對一些對資料一致性要求不高的場景,因為會存在資料不一致的可能。
我們用虛擬碼來描述下,假設2個資料來源
begin1
begin2
sql1
sql2
commit1
commit2
複製程式碼
這種方案是可以實現在sql1 sql2之間的異常回滾。如果出現commit1提交成功,commit2提交失敗(或者超時)這種情況,就會造成資料不一致,雖然這種情況概率很低,但也是一個隱患。
這個實現很類似於2PC,都會有在一個參與者執行任務提交後,另一個參與者出現異常而導致資料不一致的問題。
其實一般情況下,系統的需求只是要達到最終一致性,那就可以考慮使用TCC,對每個事物進行Try,如果執行沒有問題,再執行Confirm,如果執行過程中出了問題,則執行操作的逆操Cancel(自動化補償手段)。
但是TCC對每個事務都需要Try,再執行Confirm,略微顯得臃腫,根據不同的業務場景可以有更好的方案(比如補償模式,定期校對模式之類),具體的可以看分散式服務化系統一致性的“最佳實幹”
分散式事務實現
抽了點時間,自己實現了一個分散式事務的中介軟體,在一些對資料一致性要求高的場景可以使用。shine-mq 相應的設計思路在 分散式事務:基於可靠訊息服務
參考文獻
李豔鵬. 著. 分散式服務架構:原理、設計與實戰[M].