javascript
事务嵌套问题_注意Spring事务这一点,避免出现大事务
背景
本篇文章主要分享壓測(cè)的(高并發(fā))時(shí)候發(fā)現(xiàn)的一些問題。之前的兩篇文章已經(jīng)講述了在高并發(fā)的情況下,消息隊(duì)列和數(shù)據(jù)庫連接池的一些總結(jié)和優(yōu)化,有興趣的可以在我的公眾號(hào)中去翻閱。廢話不多說,進(jìn)入正題。
事務(wù),想必各位CRUD之王對(duì)其并不陌生,基本上有多個(gè)寫請(qǐng)求的都需要使用事務(wù),而Spring對(duì)于事務(wù)的使用又特別的簡(jiǎn)單,只需要一個(gè)@Transactional注解即可,如下面的例子:
@Transactionalpublic int createOrder(Order order){orderDbStorage.save(order);orderItemDbStorage.save(order.getItems());return order.getId();}在我們創(chuàng)建訂單的時(shí)候, 通常需要將訂單和訂單項(xiàng)放在同一個(gè)事務(wù)里面保證其滿足ACID,這里我們只需要在我們創(chuàng)建訂單的方法上面寫上事務(wù)注解即可。
事務(wù)的合理使用
對(duì)于上面的創(chuàng)建訂單的代碼,如果現(xiàn)在需要新增一個(gè)需求,在創(chuàng)建訂單之后發(fā)送一個(gè)消息到消息隊(duì)列或者調(diào)用一個(gè)RPC,你會(huì)怎么做呢?很多同學(xué)首先會(huì)想到,直接在事務(wù)方法里面進(jìn)行調(diào)用:
@Transactionalpublic int createOrder(Order order){orderDbStorage.save(order);orderItemDbStorage.save(order.getItems());sendRpc();sendMessage();return order.getId();}這種代碼在很多人寫的業(yè)務(wù)中都會(huì)出現(xiàn),事務(wù)中嵌套rpc,嵌套一些非DB的操作,一般情況下這么寫的確也沒什么問題,一旦非DB寫操作出現(xiàn)比較慢,或者流量比較大,就會(huì)出現(xiàn)大事務(wù)的問題。由于事務(wù)的一直不提交,就會(huì)導(dǎo)致數(shù)據(jù)庫連接被占用。這個(gè)時(shí)候你可能會(huì)問,我擴(kuò)大點(diǎn)數(shù)據(jù)庫連接不就行了嗎,100個(gè)不行就上1000個(gè),在上篇文章已經(jīng)講過數(shù)據(jù)庫連接池大小依然會(huì)影響我們數(shù)據(jù)庫的性能,所以,數(shù)據(jù)庫連接并不是想擴(kuò)多少擴(kuò)多少。
那我們應(yīng)該怎么對(duì)其進(jìn)行優(yōu)化呢?在這里可以仔細(xì)想想,我們的非db操作,其實(shí)是不滿足我們事務(wù)的ACID的,那么干嘛要寫在事務(wù)里面,所以這里我們可以將其提取出來。
public int createOrder(Order order){createOrderService.createOrder(order);sendRpc();sendMessage();}在這個(gè)方法里面先去調(diào)用事務(wù)的創(chuàng)建訂單,然后在去調(diào)用其他非DB操作。如果我們現(xiàn)在想要更復(fù)雜一點(diǎn)的邏輯,比如創(chuàng)建訂單成功就發(fā)送成功的RPC請(qǐng)求,失敗就發(fā)送失敗的RPC請(qǐng)求,由上面的代碼我們可以做如下轉(zhuǎn)化:
public int createOrder(Order order){try {createOrderService.createOrder(order);sendSuccessedRpc();}catch (Exception e){sendFailedRpc();throw e;}}通常我們會(huì)捕獲異常,或者根據(jù)返回值來進(jìn)行一些特殊處理,這里的實(shí)現(xiàn)需要顯示的捕獲異常,并且在次拋出,這種方式不是很優(yōu)雅,那么怎么才能更好的寫這種話邏輯呢?
TransactionSynchronizationManager
在Spring的事務(wù)中剛好提供了一些工具方法,來幫助我們完成這種需求。在TransactionSynchronizationManager中提供了讓我們對(duì)事務(wù)注冊(cè)callBack的方法:
public static void registerSynchronization(TransactionSynchronization synchronization)throws IllegalStateException {Assert.notNull(synchronization, "TransactionSynchronization must not be null");if (!isSynchronizationActive()) {throw new IllegalStateException("Transaction synchronization is not active");}synchronizations.get().add(synchronization);}TransactionSynchronization也就是我們事務(wù)的callBack,提供了一些擴(kuò)展點(diǎn)給我們:
public interface TransactionSynchronization extends Flushable {int STATUS_COMMITTED = 0;int STATUS_ROLLED_BACK = 1;int STATUS_UNKNOWN = 2;/*** 掛起時(shí)觸發(fā)*/void suspend();/*** 掛起事務(wù)拋出異常的時(shí)候 會(huì)觸發(fā)*/void resume();@Overridevoid flush();/*** 在事務(wù)提交之前觸發(fā)*/void beforeCommit(boolean readOnly);/*** 在事務(wù)完成之前觸發(fā)*/void beforeCompletion();/*** 在事務(wù)提交之后觸發(fā)*/void afterCommit();/*** 在事務(wù)完成之后觸發(fā)*/void afterCompletion(int status); }我們可以利用afterComplettion方法實(shí)現(xiàn)我們上面的業(yè)務(wù)邏輯:
@Transactionalpublic int createOrder(Order order){orderDbStorage.save(order);orderItemDbStorage.save(order.getItems());TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {@Overridepublic void afterCompletion(int status) {if (status == STATUS_COMMITTED){sendSuccessedRpc();}else {sendFailedRpc();}}});return order.getId();}這里我們直接實(shí)現(xiàn)了afterCompletion,通過事務(wù)的status進(jìn)行判斷,我們應(yīng)該具體發(fā)送哪個(gè)RPC。當(dāng)然我們可以進(jìn)一步封裝TransactionSynchronizationManager.registerSynchronization將其封裝成一個(gè)事務(wù)的Util,可以使我們的代碼更加簡(jiǎn)潔。
通過這種方式我們不必把所有非DB操作都寫在方法之外,這樣代碼更具有邏輯連貫性,更加易讀,并且優(yōu)雅。
afterCompletion的坑
這個(gè)注冊(cè)事務(wù)的回調(diào)代碼在我們?cè)谖覀兊臉I(yè)務(wù)邏輯中經(jīng)常會(huì)出現(xiàn),比如某個(gè)事務(wù)做完之后的刷新緩存,發(fā)送消息隊(duì)列,發(fā)送通知消息等等,在日常的使用中,大家用這個(gè)基本也沒出什么問題,但是在打壓的過程中,發(fā)現(xiàn)了這一塊出現(xiàn)了瓶頸,耗時(shí)特別久,通過一系列的監(jiān)測(cè),發(fā)現(xiàn)是從數(shù)據(jù)庫連接池獲取連接等待的時(shí)間較長(zhǎng),最終我們定位到了afterCompeltion這個(gè)動(dòng)作,居然沒有歸還數(shù)據(jù)庫連接。
在Spring的AbstractPlatformTransactionManager中,對(duì)commit處理的代碼如下:
private void processCommit(DefaultTransactionStatus status) throws TransactionException {try {boolean beforeCompletionInvoked = false;try {prepareForCommit(status);triggerBeforeCommit(status);triggerBeforeCompletion(status);beforeCompletionInvoked = true;boolean globalRollbackOnly = false;if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {globalRollbackOnly = status.isGlobalRollbackOnly();}if (status.hasSavepoint()) {if (status.isDebug()) {logger.debug("Releasing transaction savepoint");}status.releaseHeldSavepoint();}else if (status.isNewTransaction()) {if (status.isDebug()) {logger.debug("Initiating transaction commit");}doCommit(status);}// Throw UnexpectedRollbackException if we have a global rollback-only// marker but still didn't get a corresponding exception from commit.if (globalRollbackOnly) {throw new UnexpectedRollbackException("Transaction silently rolled back because it has been marked as rollback-only");}}// Trigger afterCommit callbacks, with an exception thrown there// propagated to callers but the transaction still considered as committed.try {triggerAfterCommit(status);}finally {triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);}}finally {cleanupAfterCompletion(status);}}這里我們只需要關(guān)注 倒數(shù)幾行代碼即可,可以發(fā)現(xiàn)我們的triggerAfterCompletion,是倒數(shù)第二個(gè)執(zhí)行邏輯,當(dāng)執(zhí)行完所有的代碼之后就會(huì)執(zhí)行我們的cleanupAfterCompletion,而我們的歸還數(shù)據(jù)庫連接也在這段代碼之中,這樣就導(dǎo)致了我們獲取數(shù)據(jù)庫連接變慢。
如何優(yōu)化
對(duì)于上面的問題如何優(yōu)化呢?這里有三種方案可以進(jìn)行優(yōu)化:
- 將非DB操作提到事務(wù)之外,這種方法也就是我們上面最原始的方法,對(duì)于一些簡(jiǎn)單的邏輯可以提取,但是對(duì)于一些復(fù)雜的邏輯,比如事務(wù)的嵌套,嵌套里面調(diào)用了afterCompletion,這樣做會(huì)增大很多工作量,并且很容易出現(xiàn)問題。
- 通過多線程異步去做,提升數(shù)據(jù)庫連接池歸還速度,這種適合于注冊(cè)afterCompletion時(shí)寫在事務(wù)最后的時(shí)候,直接將需要做的放在其它線程去做。但是如果注冊(cè)afterCompletion的時(shí)候出現(xiàn)在我們事務(wù)之間,比如嵌套事務(wù),就會(huì)導(dǎo)致我們要做的后續(xù)業(yè)務(wù)邏輯和事務(wù)并行。
- 模仿Spring事務(wù)回調(diào)注冊(cè),實(shí)現(xiàn)新的注解。上面兩種方法都有各自的弊端,所以最后我們采用了這種方法,實(shí)現(xiàn)了一個(gè)自定義注解@MethodCallBack,在使用事務(wù)的上面都打上這個(gè)注解,然后通過類似的注冊(cè)代碼進(jìn)行。
通過第三種方法基本只需要把我們注冊(cè)事務(wù)回調(diào)的地方都進(jìn)行替換就可以正常使用了。
再談大事務(wù)
說了這么久大事務(wù),到底什么才是大事務(wù)呢?簡(jiǎn)單點(diǎn)就是事務(wù)時(shí)間運(yùn)行得長(zhǎng),那么就是大事務(wù)。一般來說導(dǎo)致事務(wù)時(shí)間運(yùn)行時(shí)間長(zhǎng)的因素不外乎下面幾種:
- 數(shù)據(jù)操作得很多,比如在一個(gè)事務(wù)里面插入了很多數(shù)據(jù),那么這個(gè)事務(wù)執(zhí)行時(shí)間自然就會(huì)變得很長(zhǎng)。
- 鎖的競(jìng)爭(zhēng)大,當(dāng)所有的連接都同時(shí)對(duì)同一個(gè)數(shù)據(jù)進(jìn)行操作,那么就會(huì)出現(xiàn)排隊(duì)等待,事務(wù)時(shí)間自然就會(huì)變長(zhǎng)。
- 事務(wù)中有其他非DB操作,比如一些RPC請(qǐng)求,有些人說我的RPC很快的,不會(huì)增加事務(wù)的運(yùn)行時(shí)間,但是RPC請(qǐng)求本身就是一個(gè)不穩(wěn)定的因素,受很多因素影響,網(wǎng)絡(luò)波動(dòng),下游服務(wù)響應(yīng)緩慢,如果這些因素一旦出現(xiàn),就會(huì)有大量的事務(wù)時(shí)間很長(zhǎng),有可能導(dǎo)致Mysql掛掉,從而引起雪崩。
上面的三種情況,前面兩種可能來說不是特別常見,但是第三種事務(wù)中有很多非DB操作,這個(gè)是我們非常常見,通常出現(xiàn)這個(gè)情況的原因很多時(shí)候是我們自己習(xí)慣規(guī)范,初學(xué)者或者一些經(jīng)驗(yàn)不豐富的人寫代碼,往往會(huì)先寫一個(gè)大方法,直接在這個(gè)方法加上事務(wù)注解,然后再往里面補(bǔ)充,哪管他是什么邏輯,一把梭,就像下面這張圖一樣:
當(dāng)然還有些人是想搞什么分布式事務(wù),可惜用錯(cuò)了方法,對(duì)于分布式事務(wù)可以關(guān)注Seata,同樣可以用一個(gè)注解就能幫助你做到分布式事務(wù)。
最后
其實(shí)最后想想,為什么會(huì)出現(xiàn)這種問題呢?一般大家的理解都是會(huì)認(rèn)為都是在完成之后做的了,數(shù)據(jù)庫連接肯定早都釋放了,但是事實(shí)并非如此。所以,我們使用很多API的時(shí)候不能望文生義,如果其沒有詳細(xì)的doc,那么你應(yīng)該更加深入了解其實(shí)現(xiàn)細(xì)節(jié)。
當(dāng)然最后希望大家寫代碼之前盡量還是不要一把梭,認(rèn)真對(duì)待每一句代碼。
作者:咖啡拿鐵鏈接:https://juejin.im/post/5dce0de8e51d45400425aeb7
總結(jié)
以上是生活随笔為你收集整理的事务嵌套问题_注意Spring事务这一点,避免出现大事务的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 嵌入式OS入门笔记-以RTX为案例:一.
- 下一篇: 软考初级程序员报考指南分享