最近同事發現一個業務狀態部分更新的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應用場景:緩存
咱們先複習下Spring AOP動態代理的原理。
AOP是一種通用的編程思想,Java裏有2種實現方式:session
整個事務的加強執行過程是這樣的:
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思惟腦圖,答案就在裏面
仍是以前的一段代碼,咱們把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裏一個對象有多種狀態:
經過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的例子,咱們總結了:
若是以爲有所收穫,麻煩幫我順手點個在看吧,你的舉手之勞對我來講就是最大的鼓勵。 END~
歡迎關注個人公衆號:好奇心森林