@Transactional 事務的底層原理

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

問題一:事務沒生效

咱們先看一段實例代碼,來講明下問題:spring

@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種實現方式:session

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

Spring AOP

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

ProxyObject代理對象

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

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

可是當發生方法內調用的時候,被調用的函數Class.transactionTask()儘管看起來加了事務註解,可是並無執行代理類對應的方法ProxyClass.transactionTask(),致使註解跟沒寫同樣。
函數

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

搞清楚了原理,問題的緣由就清晰了:
這個問題的緣由從表面來講,是由於在同一個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

相關文章
相關標籤/搜索