消失的事務(wù)
端午節(jié)前偎快,組內(nèi)在討論一個問題:
一個沒有加@Transactional注解的方法囊咏,去調(diào)用一個加了@Transactional的方法株茶,會不會產(chǎn)生事務(wù)来涨?
文字蒼白,還是用代碼說話启盛。
先寫一個@Transactional的方法(本文的所有代碼蹦掐,可到Github上下載):
@Transactional
public void deleteAllAndAddOneTransactional(Customer customer) {
customerRepository.deleteAll();
if ("Yang".equals(customer.getFirstName())) {
throw new RuntimeException();
}
customerRepository.save(customer);
}
方法內(nèi)先去執(zhí)行deleteAll(),刪除表中全部數(shù)據(jù)僵闯;然后執(zhí)行save()保存數(shù)據(jù)卧抗。
這兩個方法中間,會判斷傳進來的firstName是不是等于“Yang”鳖粟,是社裆,則拋異常,用于模擬兩個數(shù)據(jù)庫操作之間可能發(fā)生的異常場景向图。
如果沒有加@Transactional注解泳秀,那么這兩個操作就不在一個事務(wù)里面,不具有原子性张漂。如果deleteAll之后拋異常晶默,那么就會導(dǎo)致只刪除不新增谨娜。
而加了@Transactional之后航攒,這兩個動作在一個事務(wù)里頭,具有原子性趴梢,要么全部成功漠畜,要么全部失敗。如果deleteAll之后拋異常坞靶,則事務(wù)回滾憔狞,恢復(fù)原先被刪除的數(shù)據(jù)。
測試一下彰阴,啟動Spring Boot工程瘾敢,首先調(diào)用findAll接口,看看數(shù)據(jù)庫中都有哪些數(shù)據(jù):
接著調(diào)用deleteAndSave接口,故意傳入firstName="Yang"簇抵,果然返回失斍於拧:
然后,在回過頭去調(diào)用findAll接口碟摆,看看數(shù)據(jù)是不是還在:
數(shù)據(jù)都在晃财,說明產(chǎn)生事務(wù)了。
上面都沒啥典蜕,都跟符合我們的直覺断盛。
問題來了,如果我的接口是去調(diào)用一個沒有加@Transactional的方法愉舔,然后這個方法再去調(diào)用加了@Transactional的方法呢钢猛?
public void deleteAllAndAddOne(Customer customer) {
System.out.println("go into deleteAllAndAddOne");
deleteAllAndAddOneTransactional(customer);
}
@Transactional
public void deleteAllAndAddOneTransactional(Customer customer) {
customerRepository.deleteAll();
if ("Yang".equals(customer.getFirstName())) {
throw new RuntimeException();
}
customerRepository.save(customer);
}
直覺告訴我,會的轩缤。
重新編譯厢洞,啟動,調(diào)用新的接口典奉,繼續(xù)故意讓它拋異常:
然后再去findAll躺翻,看看數(shù)據(jù)還在不在:
WTF! 空空如也!數(shù)據(jù)都沒了卫玖!
看來我又一次被直覺欺騙了公你。
還是得老老實實看代碼,弄懂原理假瞬。
看了一晚上代碼陕靠,恍然大悟。咱們先畫個圖解釋一下脱茉,再來看看代碼剪芥。
圖解@Transactional
首先,我們得先弄懂@Transactional的原理琴许。
為什么第一種情況税肪,也就是直接調(diào)用@Transactional方法,會產(chǎn)生事務(wù)榜田?
其實Spring的@Transactional益兄,跟Spring AOP一樣,都是利用了動態(tài)代理箭券。
我們寫了一個類净捅,里面寫了一個加了@Transactional注解的方法,這原本平淡無奇辩块,什么用也沒有蛔六,就像這樣:
關(guān)鍵在于荆永,Spring在檢查到@Transactional注解之后,給這個對象生成了一個代理對象proxy:
代理對象的methodB国章,會先開啟事務(wù)(beginTransaction)屁魏,然后再去執(zhí)行原先對象target的methodB,如果拋異常捉腥,則回滾(rollBack)氓拼,如果一切順利,則提交(commit)抵碟。
而最后注入Spring容器的桃漾,也正是這個帶有事務(wù)邏輯的代理對象。所以我們調(diào)用methodB時會產(chǎn)生事務(wù)拟逮。
現(xiàn)在撬统,我們寫了一個新方法,methodA敦迄,里頭去調(diào)用methodB:
從上面的分析恋追,可以知道,我們最終拿到的罚屋,是代理對象苦囱。
那么代理對象的methodA是長什么樣子的呢?長這樣:
由于methodA沒有加@Transactional注解脾猛,所以代理對象里面撕彤,直接就是target.methodA(),直接調(diào)用了原來對象的methodA猛拴。
這下就很清晰了羹铅,代理對象的methodA,去調(diào)用原來對象的methodA愉昆,原來對象的methodA职员,再去調(diào)用原來對象的methodB,而原來對象的methodB跛溉,是不具有事務(wù)的焊切。事務(wù)只存在于代理對象的methodB. 所以整個方法也就沒有事務(wù)了。
看看代碼
最后再來看看代碼倒谷。
只需要在deleteAllAndAddOneTransactional方法內(nèi)蛛蒙,打一個斷點糙箍,一切了然渤愁。
分別調(diào)用兩個接口,比較調(diào)用堆棧:
明顯可以看出深夯,直接調(diào)用@Transactional方法抖格,堆棧更長诺苹,而且會經(jīng)過一個叫TransactionInterceptor的攔截器。
跟著堆棧往上走雹拄,會發(fā)現(xiàn)關(guān)鍵就在于這個if-else的邏輯收奔,CglibAopProxy:
CglibAopProxy會去檢查要調(diào)用的方法,有沒有AOP調(diào)用鏈:
- 沒有滓玖,則走if里面的邏輯坪哄,直接調(diào)用target對象的方法,也就是上面間接調(diào)用@Transactional方法的情形势篡;
- 有翩肌,則走else邏輯,也就是直接調(diào)用@Transactional方法的情形禁悠。
當(dāng)然念祭,如果deleteAllAndAddOne方法被別的切面攔截,那么調(diào)用鏈chain也不會為空碍侦,也會走if邏輯粱坤,這時候是否會有事務(wù)呢?思考題瓷产。
上面貼的代碼是Cglib代理的情形站玄,JDK Proxy的,大家自行欣賞濒旦。
總結(jié)
這篇文章主要講了一個有點違反直覺的現(xiàn)象蜒什。
通過這樣一個例子,希望能夠加深大家對Spring動態(tài)代理的理解疤估。
現(xiàn)在想想灾常,面試時,為什么那么喜歡問源碼铃拇?
其實道理很簡單钞瀑,你用了這項技術(shù)、這個框架慷荔,卻不知道它是怎么實現(xiàn)的雕什,那就有可能造成錯誤的使用。
參考
- 《Spring揭秘》