為什么Spring官方推薦的@Transational還能導(dǎo)致生產(chǎn)事故
在Spring中進(jìn)行事務(wù)管理非常簡(jiǎn)單,只需要在方法上加上注解@Transactional
,Spring就可以自動(dòng)幫我們進(jìn)行事務(wù)的開啟、提交、回滾操作。甚至很多人心里已經(jīng)將Spring事務(wù)與@Transactional
劃上了等號(hào),只要有數(shù)據(jù)庫(kù)相關(guān)操作就直接給方法加上@Transactional
注解。
不瞞你說(shuō),我之前也一直是這樣,直到使用@Transactional
導(dǎo)致了一次生產(chǎn)事故,而那次生產(chǎn)事故還導(dǎo)致我當(dāng)月績(jī)效被打了D…
@Transactional
導(dǎo)致的生產(chǎn)事故
19年在公司做了一個(gè)內(nèi)部報(bào)銷的項(xiàng)目,有這樣一個(gè)業(yè)務(wù)邏輯:
1、員工加班打車可以通過(guò)滴滴出行企業(yè)版直接打車,第二天打車費(fèi)用可以直接同步到我們的報(bào)銷平臺(tái)
2、員工可以在報(bào)銷平臺(tái)勾選自己打車費(fèi)用并創(chuàng)建一張報(bào)銷單進(jìn)行報(bào)銷,創(chuàng)建報(bào)銷單的同時(shí)會(huì)創(chuàng)建一條審批流(統(tǒng)一流程平臺(tái))讓領(lǐng)導(dǎo)審批
當(dāng)時(shí)創(chuàng)建報(bào)銷單的代碼是這么寫的:
/** * 保存報(bào)銷單并創(chuàng)建工作流 */ @Transactional(rollbackFor = Exception.class) public void save(RequestBillDTO requestBillDTO){ //調(diào)用流程HTTP接口創(chuàng)建工作流 workflowUtil.createFlow("BILL",requestBillDTO); //轉(zhuǎn)換DTO對(duì)象 RequestBill requestBill = JkMappingUtils.convert(requestBillDTO, RequestBill.class); requestBillDao.save(requestBill); //保存明細(xì)表 requestDetailDao.save(requestBill.getDetail()) }
代碼非常簡(jiǎn)單也很 “優(yōu)雅”,先通過(guò)http接口調(diào)用工作流引擎創(chuàng)建審批流,然后保存報(bào)銷單,而為了保證操作的事務(wù),在整個(gè)方法上加上了@Transactional
注解(仔細(xì)想想,這樣真的能保證事務(wù)嗎?)。
報(bào)銷項(xiàng)目屬于公司內(nèi)部項(xiàng)目,本身是沒(méi)什么高并發(fā)的,系統(tǒng)也一直穩(wěn)定運(yùn)行著。
在年末的一天下午(前幾天剛好下了大雪,打車的人特別多),公司發(fā)通知郵件說(shuō)年度報(bào)銷窗口即將關(guān)閉,需要盡快將未報(bào)銷的費(fèi)用報(bào)銷掉,而剛好那天工作流引擎在進(jìn)行安全加固。
收到郵件后報(bào)銷的人開始逐漸增多,在接近下班的時(shí)候到達(dá)頂峰,此時(shí)報(bào)銷系統(tǒng)開始出現(xiàn)了故障:數(shù)據(jù)庫(kù)監(jiān)控平臺(tái)一直收到告警短信,數(shù)據(jù)庫(kù)連接不足,出現(xiàn)大量死鎖;日志顯示調(diào)用流程引擎接口出現(xiàn)大量超時(shí);同時(shí)一直提示CannotGetJdbcConnectionException
,數(shù)據(jù)庫(kù)連接池連接占滿。
在發(fā)生故障后,我們嘗試過(guò)殺掉死鎖進(jìn)程,也進(jìn)行過(guò)暴力重啟,只是不到10分鐘故障再次出現(xiàn),收到大量電話投訴。最后沒(méi)辦法只能向全員發(fā)送停機(jī)維護(hù)郵件并發(fā)送故障報(bào)告,而后,績(jī)效被打了個(gè)D,慘…。
事故原因分析
通過(guò)對(duì)日志的分析我們很容易就可以定位到故障原因就是保存報(bào)銷單的save()方法,而罪魁禍?zhǔn)拙褪悄莻€(gè)@Transactional
注解。
我們知道@Transactional
注解,是使用 AOP 實(shí)現(xiàn)的,本質(zhì)就是在目標(biāo)方法執(zhí)行前后進(jìn)行攔截。 在目標(biāo)方法執(zhí)行前加入或創(chuàng)建一個(gè)事務(wù),在執(zhí)行方法執(zhí)行后,根據(jù)實(shí)際情況選擇提交或是回滾事務(wù)。
當(dāng) Spring 遇到該注解時(shí),會(huì)自動(dòng)從數(shù)據(jù)庫(kù)連接池中獲取 connection,并開啟事務(wù)然后綁定到 ThreadLocal 上,對(duì)于@Transactional注解包裹的整個(gè)方法都是使用同一個(gè)connection連接。如果我們出現(xiàn)了耗時(shí)的操作,比如第三方接口調(diào)用,業(yè)務(wù)邏輯復(fù)雜,大批量數(shù)據(jù)處理等就會(huì)導(dǎo)致我們我們占用這個(gè)connection的時(shí)間會(huì)很長(zhǎng),數(shù)據(jù)庫(kù)連接一直被占用不釋放。一旦類似操作過(guò)多,就會(huì)導(dǎo)致數(shù)據(jù)庫(kù)連接池耗盡。
在一個(gè)事務(wù)中執(zhí)行RPC操作導(dǎo)致數(shù)據(jù)庫(kù)連接池?fù)伪瑢儆谑堑湫偷?strong>長(zhǎng)事務(wù)問(wèn)題,類似的操作還有在事務(wù)中進(jìn)行大量數(shù)據(jù)查詢,業(yè)務(wù)規(guī)則處理等…
何為長(zhǎng)事務(wù)?
顧名思義就是運(yùn)行時(shí)間比較長(zhǎng),長(zhǎng)時(shí)間未提交的事務(wù),也可以稱之為大事務(wù)。
長(zhǎng)事務(wù)會(huì)引發(fā)哪些問(wèn)題?
長(zhǎng)事務(wù)引發(fā)的常見(jiàn)危害有:
- 數(shù)據(jù)庫(kù)連接池被占滿,應(yīng)用無(wú)法獲取連接資源;
- 容易引發(fā)數(shù)據(jù)庫(kù)死鎖;
- 數(shù)據(jù)庫(kù)回滾時(shí)間長(zhǎng);
- 在主從架構(gòu)中會(huì)導(dǎo)致主從延時(shí)變大。
如何避免長(zhǎng)事務(wù)?
既然知道了長(zhǎng)事務(wù)的危害,那如何在開發(fā)中避免出現(xiàn)長(zhǎng)事務(wù)問(wèn)題呢?
很明顯,解決長(zhǎng)事務(wù)的宗旨就是 對(duì)事務(wù)方法進(jìn)行拆分,盡量讓事務(wù)變小,變快,減小事務(wù)的顆粒度。
既然提到了事務(wù)的顆粒度,我們就先回顧一下Spring進(jìn)行事務(wù)管理的方式。
聲明式事務(wù)
首先我們要知道,通過(guò)在方法上使用@Transactional
注解進(jìn)行事務(wù)管理的操作叫聲明式事務(wù) 。
使用聲明式事務(wù)的優(yōu)點(diǎn) 很明顯,就是使用很簡(jiǎn)單,可以自動(dòng)幫我們進(jìn)行事務(wù)的開啟、提交以及回滾等操作。使用這種方式,程序員只需要關(guān)注業(yè)務(wù)邏輯就可以了。
聲明式事務(wù)有一個(gè)最大的缺點(diǎn),就是事務(wù)的顆粒度是整個(gè)方法,無(wú)法進(jìn)行精細(xì)化控制。
與聲明式事務(wù)對(duì)應(yīng)的就是編程式事務(wù)。
基于底層的API,開發(fā)者在代碼中手動(dòng)的管理事務(wù)的開啟、提交、回滾等操作。在spring項(xiàng)目中可以使用TransactionTemplate
類的對(duì)象,手動(dòng)控制事務(wù)。
@Autowired private TransactionTemplate transactionTemplate; ... public void save(RequestBill requestBill) { transactionTemplate.execute(transactionStatus -> { requestBillDao.save(requestBill); //保存明細(xì)表 requestDetailDao.save(requestBill.getDetail()); return Boolean.TRUE; }); }
使用編程式事務(wù)最大的好處就是可以精細(xì)化控制事務(wù)范圍。
所以避免長(zhǎng)事務(wù)最簡(jiǎn)單的方法就是不要使用聲明式事務(wù)@Transactional
,而是使用編程式事務(wù)手動(dòng)控制事務(wù)范圍。
有的同學(xué)會(huì)說(shuō),@Transactional
使用這么簡(jiǎn)單,有沒(méi)有辦法既可以使用@Transactional
,又能避免產(chǎn)生長(zhǎng)事務(wù)?
那就需要對(duì)方法進(jìn)行拆分,將不需要事務(wù)管理的邏輯與事務(wù)操作分開:
@Service public class OrderService{ public void createOrder(OrderCreateDTO createDTO){ query(); validate(); saveData(createDTO); } //事務(wù)操作 @Transactional(rollbackFor = Throwable.class) public void saveData(OrderCreateDTO createDTO){ orderDao.insert(createDTO); } }
query()
與validate()
不需要事務(wù),我們將其與事務(wù)方法saveData()
拆開。
當(dāng)然,這種拆分會(huì)命中使用@Transactional
注解時(shí)事務(wù)不生效的經(jīng)典場(chǎng)景,很多新手非常容易犯這個(gè)錯(cuò)誤。@Transactional
注解的聲明式事務(wù)是通過(guò)spring aop起作用的,而spring aop需要生成代理對(duì)象,直接在同一個(gè)類中方法調(diào)用使用的還是原始對(duì)象,事務(wù)不生效。其他幾個(gè)常見(jiàn)的事務(wù)不生效的場(chǎng)景為:
- @Transactional 應(yīng)用在非 public 修飾的方法上
- @Transactional 注解屬性 propagation 設(shè)置錯(cuò)誤
- @Transactional 注解屬性 rollbackFor 設(shè)置錯(cuò)誤
- 同一個(gè)類中方法調(diào)用,導(dǎo)致@Transactional失效
- 異常被catch捕獲導(dǎo)致@Transactional失效
所以正確的拆分方法應(yīng)該是下面兩種:
可以將方法放入另一個(gè)類,如新增 manager層
,通過(guò)spring注入,這樣符合了在對(duì)象之間調(diào)用的條件。
@Service public class OrderService{ @Autowired private OrderManager orderManager; public void createOrder(OrderCreateDTO createDTO){ query(); validate(); orderManager.saveData(createDTO); } } @Service public class OrderManager{ @Autowired private OrderDao orderDao; @Transactional(rollbackFor = Throwable.class) public void saveData(OrderCreateDTO createDTO){ orderDao.saveData(createDTO); } }
啟動(dòng)類添加@EnableAspectJAutoProxy(exposeProxy = true)
,方法內(nèi)使用AopContext.currentProxy()
獲得代理類,使用事務(wù)。
SpringBootApplication.java @EnableAspectJAutoProxy(exposeProxy = true) @SpringBootApplication public class SpringBootApplication {}
OrderService.java public void createOrder(OrderCreateDTO createDTO){ OrderService orderService = (OrderService)AopContext.currentProxy(); orderService.saveData(createDTO); }
小結(jié)
使用@Transactional
注解在開發(fā)時(shí)確實(shí)很方便,但是稍微不注意就可能出現(xiàn)長(zhǎng)事務(wù)問(wèn)題。所以對(duì)于復(fù)雜業(yè)務(wù)邏輯,我這里更建議你使用編程式事務(wù)來(lái)管理事務(wù),當(dāng)然,如果你非要使用@Transactional
,可以根據(jù)上文提到的兩種方案進(jìn)行方法拆分。
到此這篇關(guān)于為什么Spring官方推薦的@Transational還能導(dǎo)致生產(chǎn)事故的文章就介紹到這了,更多相關(guān)Spring @Transationa 生產(chǎn)事故內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決springboot mapper注入報(bào)紅問(wèn)題
這篇文章主要介紹了解決springboot mapper注入報(bào)紅問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Mybatis如何使用正則模糊匹配多個(gè)數(shù)據(jù)
這篇文章主要介紹了Mybatis如何使用正則模糊匹配多個(gè)數(shù)據(jù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01SpringBoot實(shí)現(xiàn)多個(gè)ApplicationRunner時(shí)部分接口未執(zhí)行問(wèn)題
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)多個(gè)ApplicationRunner時(shí)部分接口未執(zhí)行問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05Bean的自動(dòng)注入及循環(huán)依賴問(wèn)題
本文詳細(xì)介紹了Bean的自動(dòng)注入及循環(huán)依賴,文中通過(guò)代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)有一定的研究?jī)r(jià)值,感興趣的小伙伴可以閱讀參考2023-03-03深入解析Java的Hibernate框架中的一對(duì)一關(guān)聯(lián)映射
這篇文章主要介紹了Java的Hibernate框架的一對(duì)一關(guān)聯(lián)映射,包括對(duì)一對(duì)一外聯(lián)映射的講解,需要的朋友可以參考下2016-01-01