天天看點

Spring官方推薦的@Transactional還能導緻生産事故?

作者:架構師之道

在Spring中進行事務管理非常簡單,隻需要在方法上加上注解@Transactional,Spring就可以自動幫我們進行事務的開啟、送出、復原操作。甚至很多人心裡已經将Spring事務與@Transactional劃上了等号,隻要有資料庫相關操作就直接給方法加上@Transactional注解。

不瞞你說,我之前也一直是這樣,直到使用@Transactional導緻了一次生産事故,而那次生産事故還導緻我當月績效被打了D...

@Transactional導緻的生産事故

19年在公司做了一個内部報帳的項目,有這樣一個業務邏輯:

1、員工加班打車可以通過滴滴出行企業版直接打車,第二天打車費用可以直接同步到我們的報帳平台

2、員工可以在報帳平台勾選自己打車費用并建立一張報帳單進行報帳,建立報帳單的同時會建立一條審批流(統一流程平台)讓上司審批

當時建立報帳單的代碼是這麼寫的:

/**
 * 儲存報帳單并建立工作流
 */
@Transactional(rollbackFor = Exception.class)
public void save(RequestBillDTO requestBillDTO){
     //調用流程HTTP接口建立工作流
    workflowUtil.createFlow("BILL",requestBillDTO);
    
    //轉換DTO對象
    RequestBill requestBill = JkMappingUtils.convert(requestBillDTO, RequestBill.class);
    requestBillDao.save(requestBill);
    //儲存明細表
    requestDetailDao.save(requestBill.getDetail())
}
           

代碼非常簡單也很 “優雅”,先通過http接口調用工作流引擎建立審批流,然後儲存報帳單,而為了保證操作的事務,在整個方法上加上了@Transactional注解(仔細想想,這樣真的能保證事務嗎?)。

報帳項目屬于公司内部項目,本身是沒什麼高并發的,系統也一直穩定運作着。

在年末的一天下午(前幾天剛好下了大雪,打車的人特别多),公司發通知郵件說年度報帳視窗即将關閉,需要盡快将未報帳的費用報帳掉,而剛好那天工作流引擎在進行安全加強。

收到郵件後報帳的人開始逐漸增多,在接近下班的時候到達頂峰,此時報帳系統開始出現了故障:資料庫監控平台一直收到告警短信,資料庫連接配接不足,出現大量死鎖;日志顯示調用流程引擎接口出現大量逾時;同時一直提示CannotGetJdbcConnectionException,資料庫連接配接池連接配接占滿。

在發生故障後,我們嘗試過殺掉死鎖程序,也進行過暴力重新開機,隻是不到10分鐘故障再次出現,收到大量電話投訴。

最後沒辦法隻能向全員發送停機維護郵件并發送故障報告,而後,績效被打了個D,慘...。

事故原因分析

通過對日志的分析我們很容易就可以定位到故障原因就是儲存報帳單的save()方法,而罪魁禍首就是那個@Transactional注解。

我們知道@Transactional 注解,是使用 AOP 實作的,本質就是在目标方法執行前後進行攔截。在目标方法執行前加入或建立一個事務,在執行方法執行後,根據實際情況選擇送出或是復原事務。

當 Spring 遇到該注解時,會自動從資料庫連接配接池中擷取 connection,并開啟事務然後綁定到 ThreadLocal 上,對于@Transactional注解包裹的整個方法都是使用同一個connection連接配接。如果我們出現了耗時的操作,比如第三方接口調用,業務邏輯複雜,大批量資料處理等就會導緻我們我們占用這個connection的時間會很長,資料庫連接配接一直被占用不釋放。一旦類似操作過多,就會導緻資料庫連接配接池耗盡。

在一個事務中執行RPC操作導緻資料庫連接配接池撐爆屬于是典型的長事務問題,類似的操作還有在事務中進行大量資料查詢,業務規則處理等...

何為長事務?

顧名思義就是運作時間比較長,長時間未送出的事務,也可以稱之為大事務。

長事務會引發哪些問題?

長事務引發的常見危害有:

  1. 資料庫連接配接池被占滿,應用無法擷取連接配接資源;
  2. 容易引發資料庫死鎖;
  3. 資料庫復原時間長;
  4. 在主從架構中會導緻主從延時變大。

如何避免長事務?

既然知道了長事務的危害,那如何在開發中避免出現長事務問題呢?

很明顯,解決長事務的宗旨就是 對事務方法進行拆分,盡量讓事務變小,變快,減小事務的顆粒度。

既然提到了事務的顆粒度,我們就先回顧一下Spring進行事務管理的方式。

聲明式事務

首先我們要知道,通過在方法上使用@Transactional注解進行事務管理的操作叫聲明式事務 。

使用聲明式事務的優點 很明顯,就是使用很簡單,可以自動幫我們進行事務的開啟、送出以及復原等操作。使用這種方式,程式員隻需要關注業務邏輯就可以了。

聲明式事務有一個最大的缺點,就是事務的顆粒度是整個方法,無法進行精細化控制。

與聲明式事務對應的就是程式設計式事務。

基于底層的API,開發者在代碼中手動的管理事務的開啟、送出、復原等操作。在spring項目中可以使用TransactionTemplate類的對象,手動控制事務。

@Autowired 
private TransactionTemplate transactionTemplate; 
 
... 

public void save(RequestBill requestBill) { 
    transactionTemplate.execute(transactionStatus -> {
        requestBillDao.save(requestBill);
        //儲存明細表
        requestDetailDao.save(requestBill.getDetail());
        return Boolean.TRUE; 
    });
} 
           

使用程式設計式事務最大的好處就是可以精細化控制事務範圍。

是以避免長事務最簡單的方法就是不要使用聲明式事務@Transactional,而是使用程式設計式事務手動控制事務範圍。

有的同學會說,@Transactional使用這麼簡單,有沒有辦法既可以使用@Transactional,又能避免産生長事務?

Spring官方推薦的@Transactional還能導緻生産事故?

那就需要對方法進行拆分,将不需要事務管理的邏輯與事務操作分開:

@Service
public class OrderService{

    public void createOrder(OrderCreateDTO createDTO){
        query();
        validate();
        saveData(createDTO);
    }
  
  //事務操作
    @Transactional(rollbackFor = Throwable.class)
    public void saveData(OrderCreateDTO createDTO){
        orderDao.insert(createDTO);
    }
}
           

query()與validate()不需要事務,我們将其與事務方法saveData()拆開。

當然,這種拆分會命中使用@Transactional注解時事務不生效的經典場景,很多新手非常容易犯這個錯誤。@Transactional注解的聲明式事務是通過spring aop起作用的,而spring aop需要生成代理對象,直接在同一個類中方法調用使用的還是原始對象,事務不生效。其他幾個常見的事務不生效的場景為:

@Transactional 應用在非 public 修飾的方法上

@Transactional 注解屬性 propagation 設定錯誤

@Transactional 注解屬性 rollbackFor 設定錯誤

同一個類中方法調用,導緻@Transactional失效

異常被catch捕獲導緻@Transactional失效

正确的拆分方法應該使用下面兩種:

  1. 可以将方法放入另一個類,如新增 manager層,通過spring注入,這樣符合了在對象之間調用的條件。
@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);
    }
}
           
  1. 啟動類添加@EnableAspectJAutoProxy(exposeProxy = true),方法内使用AopContext.currentProxy()獲得代理類,使用事務。
SpringBootApplication.java

@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class SpringBootApplication {}

           
OrderService.java
  
public void createOrder(OrderCreateDTO createDTO){
    OrderService orderService = (OrderService)AopContext.currentProxy();
    orderService.saveData(createDTO);
}           

小結

使用@Transactional注解在開發時确實很友善,但是稍微不注意就可能出現長事務問題。是以對于複雜業務邏輯,我這裡更建議你使用程式設計式事務來管理事務,當然,如果你非要使用@Transactional,可以根據上文提到的兩種方案進行方法拆分。

來源:https://mp.weixin.qq.com/s/LOWsuE8plvhToYoh43wfkg

作者:飄渺Java