javascript
事务嵌套问题_注意Spring事务这一点,避免出现大事务
背景
本篇文章主要分享壓測的(高并發(fā))時候發(fā)現(xiàn)的一些問題。之前的兩篇文章已經(jīng)講述了在高并發(fā)的情況下,消息隊列和數(shù)據(jù)庫連接池的一些總結和優(yōu)化,有興趣的可以在我的公眾號中去翻閱。廢話不多說,進入正題。
事務,想必各位CRUD之王對其并不陌生,基本上有多個寫請求的都需要使用事務,而Spring對于事務的使用又特別的簡單,只需要一個@Transactional注解即可,如下面的例子:
@Transactionalpublic int createOrder(Order order){orderDbStorage.save(order);orderItemDbStorage.save(order.getItems());return order.getId();}在我們創(chuàng)建訂單的時候, 通常需要將訂單和訂單項放在同一個事務里面保證其滿足ACID,這里我們只需要在我們創(chuàng)建訂單的方法上面寫上事務注解即可。
事務的合理使用
對于上面的創(chuàng)建訂單的代碼,如果現(xiàn)在需要新增一個需求,在創(chuàng)建訂單之后發(fā)送一個消息到消息隊列或者調用一個RPC,你會怎么做呢?很多同學首先會想到,直接在事務方法里面進行調用:
@Transactionalpublic int createOrder(Order order){orderDbStorage.save(order);orderItemDbStorage.save(order.getItems());sendRpc();sendMessage();return order.getId();}這種代碼在很多人寫的業(yè)務中都會出現(xiàn),事務中嵌套rpc,嵌套一些非DB的操作,一般情況下這么寫的確也沒什么問題,一旦非DB寫操作出現(xiàn)比較慢,或者流量比較大,就會出現(xiàn)大事務的問題。由于事務的一直不提交,就會導致數(shù)據(jù)庫連接被占用。這個時候你可能會問,我擴大點數(shù)據(jù)庫連接不就行了嗎,100個不行就上1000個,在上篇文章已經(jīng)講過數(shù)據(jù)庫連接池大小依然會影響我們數(shù)據(jù)庫的性能,所以,數(shù)據(jù)庫連接并不是想擴多少擴多少。
那我們應該怎么對其進行優(yōu)化呢?在這里可以仔細想想,我們的非db操作,其實是不滿足我們事務的ACID的,那么干嘛要寫在事務里面,所以這里我們可以將其提取出來。
public int createOrder(Order order){createOrderService.createOrder(order);sendRpc();sendMessage();}在這個方法里面先去調用事務的創(chuàng)建訂單,然后在去調用其他非DB操作。如果我們現(xiàn)在想要更復雜一點的邏輯,比如創(chuàng)建訂單成功就發(fā)送成功的RPC請求,失敗就發(fā)送失敗的RPC請求,由上面的代碼我們可以做如下轉化:
public int createOrder(Order order){try {createOrderService.createOrder(order);sendSuccessedRpc();}catch (Exception e){sendFailedRpc();throw e;}}通常我們會捕獲異常,或者根據(jù)返回值來進行一些特殊處理,這里的實現(xiàn)需要顯示的捕獲異常,并且在次拋出,這種方式不是很優(yōu)雅,那么怎么才能更好的寫這種話邏輯呢?
TransactionSynchronizationManager
在Spring的事務中剛好提供了一些工具方法,來幫助我們完成這種需求。在TransactionSynchronizationManager中提供了讓我們對事務注冊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也就是我們事務的callBack,提供了一些擴展點給我們:
public interface TransactionSynchronization extends Flushable {int STATUS_COMMITTED = 0;int STATUS_ROLLED_BACK = 1;int STATUS_UNKNOWN = 2;/*** 掛起時觸發(fā)*/void suspend();/*** 掛起事務拋出異常的時候 會觸發(fā)*/void resume();@Overridevoid flush();/*** 在事務提交之前觸發(fā)*/void beforeCommit(boolean readOnly);/*** 在事務完成之前觸發(fā)*/void beforeCompletion();/*** 在事務提交之后觸發(fā)*/void afterCommit();/*** 在事務完成之后觸發(fā)*/void afterCompletion(int status); }我們可以利用afterComplettion方法實現(xiàn)我們上面的業(yè)務邏輯:
@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();}這里我們直接實現(xiàn)了afterCompletion,通過事務的status進行判斷,我們應該具體發(fā)送哪個RPC。當然我們可以進一步封裝TransactionSynchronizationManager.registerSynchronization將其封裝成一個事務的Util,可以使我們的代碼更加簡潔。
通過這種方式我們不必把所有非DB操作都寫在方法之外,這樣代碼更具有邏輯連貫性,更加易讀,并且優(yōu)雅。
afterCompletion的坑
這個注冊事務的回調代碼在我們在我們的業(yè)務邏輯中經(jīng)常會出現(xiàn),比如某個事務做完之后的刷新緩存,發(fā)送消息隊列,發(fā)送通知消息等等,在日常的使用中,大家用這個基本也沒出什么問題,但是在打壓的過程中,發(fā)現(xiàn)了這一塊出現(xiàn)了瓶頸,耗時特別久,通過一系列的監(jiān)測,發(fā)現(xiàn)是從數(shù)據(jù)庫連接池獲取連接等待的時間較長,最終我們定位到了afterCompeltion這個動作,居然沒有歸還數(shù)據(jù)庫連接。
在Spring的AbstractPlatformTransactionManager中,對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);}}這里我們只需要關注 倒數(shù)幾行代碼即可,可以發(fā)現(xiàn)我們的triggerAfterCompletion,是倒數(shù)第二個執(zhí)行邏輯,當執(zhí)行完所有的代碼之后就會執(zhí)行我們的cleanupAfterCompletion,而我們的歸還數(shù)據(jù)庫連接也在這段代碼之中,這樣就導致了我們獲取數(shù)據(jù)庫連接變慢。
如何優(yōu)化
對于上面的問題如何優(yōu)化呢?這里有三種方案可以進行優(yōu)化:
- 將非DB操作提到事務之外,這種方法也就是我們上面最原始的方法,對于一些簡單的邏輯可以提取,但是對于一些復雜的邏輯,比如事務的嵌套,嵌套里面調用了afterCompletion,這樣做會增大很多工作量,并且很容易出現(xiàn)問題。
- 通過多線程異步去做,提升數(shù)據(jù)庫連接池歸還速度,這種適合于注冊afterCompletion時寫在事務最后的時候,直接將需要做的放在其它線程去做。但是如果注冊afterCompletion的時候出現(xiàn)在我們事務之間,比如嵌套事務,就會導致我們要做的后續(xù)業(yè)務邏輯和事務并行。
- 模仿Spring事務回調注冊,實現(xiàn)新的注解。上面兩種方法都有各自的弊端,所以最后我們采用了這種方法,實現(xiàn)了一個自定義注解@MethodCallBack,在使用事務的上面都打上這個注解,然后通過類似的注冊代碼進行。
通過第三種方法基本只需要把我們注冊事務回調的地方都進行替換就可以正常使用了。
再談大事務
說了這么久大事務,到底什么才是大事務呢?簡單點就是事務時間運行得長,那么就是大事務。一般來說導致事務時間運行時間長的因素不外乎下面幾種:
- 數(shù)據(jù)操作得很多,比如在一個事務里面插入了很多數(shù)據(jù),那么這個事務執(zhí)行時間自然就會變得很長。
- 鎖的競爭大,當所有的連接都同時對同一個數(shù)據(jù)進行操作,那么就會出現(xiàn)排隊等待,事務時間自然就會變長。
- 事務中有其他非DB操作,比如一些RPC請求,有些人說我的RPC很快的,不會增加事務的運行時間,但是RPC請求本身就是一個不穩(wěn)定的因素,受很多因素影響,網(wǎng)絡波動,下游服務響應緩慢,如果這些因素一旦出現(xiàn),就會有大量的事務時間很長,有可能導致Mysql掛掉,從而引起雪崩。
上面的三種情況,前面兩種可能來說不是特別常見,但是第三種事務中有很多非DB操作,這個是我們非常常見,通常出現(xiàn)這個情況的原因很多時候是我們自己習慣規(guī)范,初學者或者一些經(jīng)驗不豐富的人寫代碼,往往會先寫一個大方法,直接在這個方法加上事務注解,然后再往里面補充,哪管他是什么邏輯,一把梭,就像下面這張圖一樣:
當然還有些人是想搞什么分布式事務,可惜用錯了方法,對于分布式事務可以關注Seata,同樣可以用一個注解就能幫助你做到分布式事務。
最后
其實最后想想,為什么會出現(xiàn)這種問題呢?一般大家的理解都是會認為都是在完成之后做的了,數(shù)據(jù)庫連接肯定早都釋放了,但是事實并非如此。所以,我們使用很多API的時候不能望文生義,如果其沒有詳細的doc,那么你應該更加深入了解其實現(xiàn)細節(jié)。
當然最后希望大家寫代碼之前盡量還是不要一把梭,認真對待每一句代碼。
作者:咖啡拿鐵鏈接:https://juejin.im/post/5dce0de8e51d45400425aeb7
總結
以上是生活随笔為你收集整理的事务嵌套问题_注意Spring事务这一点,避免出现大事务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 嵌入式OS入门笔记-以RTX为案例:一.
- 下一篇: gradle idea java ssm