大意了,使用@Transactional竟然出現(xiàn)了長事務(wù),導(dǎo)致生產(chǎn)事故!
大家好,我是程序汪,有粉絲經(jīng)常問面試時問生產(chǎn)問題怎么辦,自己也沒解決過什么生產(chǎn)問題,今天分享一個嚴重的生產(chǎn)問題,大家可以學習下
@Transactional導(dǎo)致的生產(chǎn)事故事故原因分析 如何避免長事務(wù)? 小結(jié)

在Spring中進行事務(wù)管理非常簡單,只需要在方法上加上注解@Transactional,Spring就可以自動幫我們進行事務(wù)的開啟、提交、回滾操作。甚至很多人心里已經(jīng)將Spring事務(wù)與@Transactional劃上了等號,只要有數(shù)據(jù)庫相關(guān)操作就直接給方法加上@Transactional注解。
不瞞你說,我之前也一直是這樣,直到使用@Transactional導(dǎo)致了一次生產(chǎn)事故,而那次生產(chǎn)事故還導(dǎo)致我當月績效被打了D...
@Transactional導(dǎo)致的生產(chǎn)事故
19年在公司做了一個內(nèi)部報銷的項目,有這樣一個業(yè)務(wù)邏輯:
1、員工加班打車可以通過滴滴出行企業(yè)版直接打車,第二天打車費用可以直接同步到我們的報銷平臺
2、員工可以在報銷平臺勾選自己打車費用并創(chuàng)建一張報銷單進行報銷,創(chuàng)建報銷單的同時會創(chuàng)建一條審批流(統(tǒng)一流程平臺)讓領(lǐng)導(dǎo)審批
當時創(chuàng)建報銷單的代碼是這么寫的:
?/**
?*?保存報銷單并創(chuàng)建工作流
?*/
@Transactional(rollbackFor?=?Exception.class)
public?void?save(RequestBillDTO?requestBillDTO){
?????//調(diào)用流程HTTP接口創(chuàng)建工作流
????workflowUtil.createFlow("BILL",requestBillDTO);
????
????//轉(zhuǎn)換DTO對象
????RequestBill?requestBill?=?JkMappingUtils.convert(requestBillDTO,?RequestBill.class);
????requestBillDao.save(requestBill);
????//保存明細表
????requestDetailDao.save(requestBill.getDetail())
}
代碼非常簡單也很 “優(yōu)雅 ”,先通過http接口調(diào)用工作流引擎創(chuàng)建審批流,然后保存報銷單,而為了保證 操作的事務(wù),在整個方法上加上了@Transactional注解(仔細想想,這樣真的能保證事務(wù)嗎? )。
報銷項目屬于公司內(nèi)部項目,本身是沒什么高并發(fā)的,系統(tǒng)也一直穩(wěn)定運行著。
在年末的一天下午(前幾天剛好下了大雪,打車的人特別多),公司發(fā)通知郵件說年度報銷窗口即將關(guān)閉,需要盡快將未報銷的費用報銷掉,而剛好那天工作流引擎在進行安全加固。
收到郵件后報銷的人開始逐漸增多,在接近下班的時候到達頂峰,此時報銷系統(tǒng)開始出現(xiàn)了故障:數(shù)據(jù)庫監(jiān)控平臺一直收到告警短信,數(shù)據(jù)庫連接不足,出現(xiàn)大量死鎖;日志顯示調(diào)用流程引擎接口出現(xiàn)大量超時;同時一直提示CannotGetJdbcConnectionException,數(shù)據(jù)庫連接池連接占滿。
在發(fā)生故障后,我們嘗試過殺掉死鎖進程,也進行過暴力重啟,只是不到10分鐘故障再次出現(xiàn),收到大量電話投訴。最后沒辦法只能向全員發(fā)送停機維護郵件并發(fā)送故障報告,而后,績效被打了個D,慘...。
事故原因分析
通過對日志的分析我們很容易就可以定位到故障原因就是保存報銷單的save()方法,而罪魁禍首就是那個@Transactional注解。
我們知道@Transactional 注解,是使用 AOP 實現(xiàn)的,本質(zhì)就是在目標方法執(zhí)行前后進行攔截。在目標方法執(zhí)行前加入或創(chuàng)建一個事務(wù),在執(zhí)行方法執(zhí)行后,根據(jù)實際情況選擇提交或是回滾事務(wù)。
當 Spring 遇到該注解時,會自動從數(shù)據(jù)庫連接池中獲取 connection,并開啟事務(wù)然后綁定到 ThreadLocal 上,對于@Transactional注解包裹的整個方法都是使用同一個connection連接 。如果我們出現(xiàn)了耗時的操作,比如第三方接口調(diào)用,業(yè)務(wù)邏輯復(fù)雜,大批量數(shù)據(jù)處理等就會導(dǎo)致我們我們占用這個connection的時間會很長,數(shù)據(jù)庫連接一直被占用不釋放。一旦類似操作過多,就會導(dǎo)致數(shù)據(jù)庫連接池耗盡。
在一個事務(wù)中執(zhí)行RPC操作導(dǎo)致數(shù)據(jù)庫連接池撐爆屬于是典型的長事務(wù)問題 ,類似的操作還有在事務(wù)中進行大量數(shù)據(jù)查詢,業(yè)務(wù)規(guī)則處理等...
何為長事務(wù)?
顧名思義就是運行時間比較長,長時間未提交的事務(wù),也可以稱之為大事務(wù) 。
長事務(wù)會引發(fā)哪些問題?
長事務(wù)引發(fā)的常見危害有:
數(shù)據(jù)庫連接池被占滿,應(yīng)用無法獲取連接資源; 容易引發(fā)數(shù)據(jù)庫死鎖; 數(shù)據(jù)庫回滾時間長; 在主從架構(gòu)中會導(dǎo)致主從延時變大。
如何避免長事務(wù)?
既然知道了長事務(wù)的危害,那如何在開發(fā)中避免出現(xiàn)長事務(wù)問題呢?
很明顯,解決長事務(wù)的宗旨就是 對事務(wù)方法進行拆分,盡量讓事務(wù)變小,變快,減小事務(wù)的顆粒度。
既然提到了事務(wù)的顆粒度,我們就先回顧一下Spring進行事務(wù)管理的方式。
聲明式事務(wù)
首先我們要知道,通過在方法上使用@Transactional注解進行事務(wù)管理的操作叫聲明式事務(wù) 。
使用聲明式事務(wù)的優(yōu)點 很明顯,就是使用很簡單,可以自動幫我們進行事務(wù)的開啟、提交以及回滾等操作。使用這種方式,程序員只需要關(guān)注業(yè)務(wù)邏輯就可以了。
聲明式事務(wù)有一個最大的缺點 ,就是事務(wù)的顆粒度是整個方法,無法進行精細化控制。
與聲明式事務(wù)對應(yīng)的就是編程式事務(wù) 。
基于底層的API,開發(fā)者在代碼中手動的管理事務(wù)的開啟、提交、回滾等操作。在spring項目中可以使用TransactionTemplate類的對象,手動控制事務(wù)。
@Autowired?
private?TransactionTemplate?transactionTemplate;?
?
...?
public?void?save(RequestBill?requestBill)?{?
????transactionTemplate.execute(transactionStatus?->?{
????????requestBillDao.save(requestBill);
????????//保存明細表
????????requestDetailDao.save(requestBill.getDetail());
????????return?Boolean.TRUE;?
????});
}?
使用編程式事務(wù)最大的好處就是可以精細化控制事務(wù)范圍。
所以避免長事務(wù)最簡單的方法就是不要使用聲明式事務(wù)@Transactional,而是使用編程式事務(wù)手動控制事務(wù)范圍。
有的同學會說,@Transactional使用這么簡單,有沒有辦法既可以使用@Transactional,又能避免產(chǎn)生長事務(wù)?

那就需要對方法進行拆分,將不需要事務(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()拆開。
當然,這種拆分會命中使用@Transactional注解時事務(wù)不生效的經(jīng)典場景,很多新手非常容易犯這個錯誤。@Transactional注解的聲明式事務(wù)是通過spring aop起作用的,而spring aop需要生成代理對象,直接在同一個類中方法調(diào)用使用的還是原始對象,事務(wù)不生效。其他幾個常見的事務(wù)不生效的場景為:
“
@Transactional 應(yīng)用在非 public 修飾的方法上 @Transactional 注解屬性 propagation 設(shè)置錯誤 @Transactional 注解屬性 rollbackFor 設(shè)置錯誤 同一個類中方法調(diào)用,導(dǎo)致@Transactional失效 異常被catch捕獲導(dǎo)致@Transactional失效 ”
正確的拆分方法應(yīng)該使用下面兩種:
可以將方法放入另一個類,如新增 manager層,通過spring注入,這樣符合了在對象之間調(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);
????}
}
啟動類添加 @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ā)時確實很方便,但是稍微不注意就可能出現(xiàn)長事務(wù)問題。所以對于復(fù)雜業(yè)務(wù)邏輯,我這里更建議你使用編程式事務(wù)來管理事務(wù),當然,如果你非要使用@Transactional,可以根據(jù)上文提到的兩種方案進行方法拆分。
程序汪資料鏈接
堪稱神級的Spring Boot手冊,從基礎(chǔ)入門到實戰(zhàn)進階
臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載!
臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開放下載!
字節(jié)跳動總結(jié)的設(shè)計模式 PDF 火了,完整版開放下載!
歡迎添加程序汪個人微信 itwang009? 進粉絲群或圍觀朋友圈
