@Transactional 事務的底層原理


最近同事發現一個業務狀態部分更新的bug,這個bug會導致兩張表的數據一致性問題。花了些時間去查問題的原因,現在總結下里面遇到的知識點原理。

問題一:事務沒生效

我們先看一段實例代碼,來說明下問題:

@Service
public class PaymentServiceImpl implements PaymentService {
    public void fetchLatestStatus(String trxId) {
      //1. do RPC request and get the payment status
      StatusResponse response = doRPC(trxId);
      //2. save request data
      saveRequest(response);
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void updatePayment(StatusResponse response) {
      Payment pay = payRepository.findByTrxId(response.getTrxId);
      //do something to update payment record by response and persist
      pay.setStatus(success);
      payRepository.save(pay);
    }
}

在上面代理里,updatePayment方法的@Transactional注解會失效,並沒有新開一個事務去保存Payment對象。

開發中少不了用到事務注解@Transactional來管理事務,@Transactional注解底層是基於Spring AOP來進行實現的。

我們來看兩個典型的AOP應用場景:

  • 統一的驗證用戶邏輯

AOP場景一

  • 反復使用的開啟事務,關閉事務邏輯

AOP場景二

原理分析

我們先復習下Spring AOP動態代理的原理。
AOP是一種通用的編程思想,Java里有2種實現方式:

  • Spring AOP,基於動態代理實現
    • JDK代理
    • Cglib代理
  • AspectJ,基於編譯期實現

Spring AOP

  1. Spring實現AOP的方法則就是利用了動態代理機制實現的;
  2. 在應用系統調用聲明@Transactional 的目標方法時,Spring Framework 默認使用 AOP 代理,在代碼運行時生成一個代理對象ProxyObject,如:

ProxyObject代理對象

整個事務的增強執行過程是這樣的:

如上圖所示 TransactionInterceptor (事務攔截器)在目標方法執行前后進行攔截,DynamicAdvisedInterceptor(CglibAopProxy 的內部類)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法會間接調用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,獲取Transactional 注解的事務配置信息。

但是當發生方法內調用的時候,被調用的函數Class.transactionTask()盡管看起來加了事務注解,但是並沒有執行代理類對應的方法ProxyClass.transactionTask(),導致注解跟沒寫一樣。

@Transactional注解加在private修飾的方法也會一樣的現象,原理其實一樣的。

搞清楚了原理,問題的原因就清晰了:
這個問題的原因從表面來說,是因為在同一個Class內,非代理增強方法中調用了被@Transactional注解增強的方法,注解會失效。背后的實際原因是Spring AOP是基於代理,同一個類內這樣調用的話,只有第一次調用了動態代理生成的ProxyClass,之后調用是不帶任何切面信息的方法本身,因為沒有直接調用Spring生成的代理對象。

解決方法

updatePayment方法放到另外一個類里,讓Spring自動為其生成代理對象,調用方就能調用到updatePayment對應的ProxyObject的方法了。

思考

我們還提到了AspectJ也是實現AOP的一種方式,那么AspectJ有這樣的方法內調用失效問題嗎?

可以關注**好奇心森林**公眾號后台回復AOP,索取我總結的AOP思維腦圖,答案就在里面

問題二:定時器運行沒啟動em

還是之前的一段代碼,我們把updatePayment方法放在一個單獨的類里。會發現之前payRepository.save(pay)必須顯式聲明保存,但是如果抽出來后就不用再寫也能自動保存。

@Service
public class PaymentServiceImpl implements PaymentService {
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void updatePayment(StatusResponse response) {
      Payment pay = payRepository.findByTrxId(response.getTrxId);
      //do something to update payment record by response and persist
      pay.setStatus(success);
      //payRepository.save(pay);
      xxxRepository.save(xxx);
    }
}

這個區別需要知道Hibernet對Entity的狀態管理機制,在Hibernet里一個對象有多種狀態:

  • Transient 瞬時態:直接new出來的對象,既沒有被保存到數據庫中,也不處於session緩存中
  • Persistent 持久態:已經被保存到數據庫中並且加入到session緩存中
  • Detached 游離態:已經被保存到數據庫中但不處於session緩存中

通過findByTrxId查出來的Payment對象處於托管態,任何改變pay對象的操作比如pay.setStatus()都會在事務結束的時候自動提交。

另外同事發現一個有趣的區別:

在Controller調用PaymentServiceImpl.updatePayment()不需要顯式保存pay對象,也能持久化到數據庫,然而用Spring的定時器調用就不會生效。

經過Debug發現,Spring框架在每個request通過OpenEntityManagerInViewInterceptorpreHandle方法里為每個request都建了一個EntityManager, 具體參見Spring源碼:

在Spring配置里加上spring.jpa.open-in-view=false 就會關閉每個request的EntityManager,通過controller調用就和定時器現象一樣了。

Open Session In View簡稱OSIV,是為了解決在mvc的controller中使用了hibernate的lazy load的屬性時沒有session拋出的LazyInitializationException異常。

對hibernate來說ToMany關系默認是延遲加載,而ToOne關系則默認是立即加載;而在mvc的controller中脫離了persisent contenxt,於是entity變成了detached狀態,這個時候要使用延遲加載的屬性時就會拋出LazyInitializationException異常,而Open Session In View 旨在解決這個問題。

Tips:

通過OSIV技術來解決LazyInitialization問題會導致open的session生命周期過長,它貫穿整個request,在view渲染完之后才能關閉session釋放數據庫連接;另外OSIV將service層的技術細節暴露到了controller層,造成了一定的耦合,因而不建議開啟,對應的解決方案就是在controller層中使用dto,而非detached狀態的entity,所需的數據不再依賴延時加載,在組裝dto的時候根據需要顯式查詢。

總結

通過一個bug的例子,我們總結了:

  • @Transactional 的底層實現
  • Spring AOP的不同實現方式和原理
  • Hibernet的對象生命周期
  • Spring的OSIV機制的目的和弊端

如果覺得有所收獲,麻煩幫我順手點個在看吧,你的舉手之勞對我來說就是最大的鼓勵。 END~

歡迎關注我的公眾號:好奇心森林
Wechat


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM