最近同事發現一個業務狀態部分更新的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應用場景:
- 統一的驗證用戶邏輯
- 反復使用的開啟事務,關閉事務邏輯
原理分析
我們先復習下Spring AOP動態代理的原理。
AOP是一種通用的編程思想,Java里有2種實現方式:
- Spring AOP,基於動態代理實現
- JDK代理
- Cglib代理
- AspectJ,基於編譯期實現
- Spring實現AOP的方法則就是利用了動態代理機制實現的;
- 在應用系統調用聲明@Transactional 的目標方法時,Spring Framework 默認使用 AOP 代理,在代碼運行時生成一個代理對象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通過OpenEntityManagerInViewInterceptor
的preHandle
方法里為每個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~
歡迎關注我的公眾號:好奇心森林