Spring事務長了個腿?輕鬆掌握技巧告別長事務煩惱!

張哥說技術發表於2023-12-26

來源:JAVA日知錄

大家好,我是飄渺。今天繼續DDD&微服務專欄。

在之前的文章 基於DDD的訂單建立 流程中,我們留下了一個問題:在createOrder()方法中,我將呼叫遠端介面獲取購物車詳情、遠端庫存校驗、訂單儲存放在一個事務中,顯然這並不是一個正確的做法,因為它會導致長事務,今天就讓我們來解決這個問題。

Spring事務長了個腿?輕鬆掌握技巧告別長事務煩惱!

為什麼會產生長事務

首先,讓我們來分析一下產生長事務的原因。

在Spring中,@Transactional註解是基於AOP實現的,本質上是在目標方法執行前後進行攔截。在目標方法執行前加入或建立一個事務,在方法執行後,根據實際情況選擇提交或回滾事務。

當Spring遇到該註解時,會自動從資料庫連線池中獲取連線並開啟事務,然後繫結到ThreadLocal上,對於@Transactional註解包裹的整個方法都是使用同一個連線。如果出現耗時的操作,如第三方介面呼叫、業務邏輯複雜、大批次資料處理等,就會導致佔用連線的時間很長,資料庫連線一直被佔用不釋放。一旦類似操作過多,就會導致資料庫連線池耗盡。

在開頭的例項中,一個事務中執行RPC操作是典型的長事務問題。類似的操作還包括在事務中進行大量資料查詢、業務規則處理等。

長事務會產生哪些問題

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

  1. 資料庫連線池被佔滿,應用無法獲取連線資源;
  2. 容易引發資料庫死鎖;
  3. 資料庫回滾時間長;
  4. 在主從架構中會導致主從延時變大。

如何避免長事務

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

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

程式設計式事務

因此,我們可以採用程式設計式事務替代宣告式事務@Transactional。在Spring專案中,可以注入TransactionTemplate物件,然後手動控制事務範圍。改造過後的程式碼如下所示:

public String createOrder(OrderCreateRequest orderCreateRequest) {
 // 獲取購物車詳情
 ShoppingCartDetailDTO shoppingCartDetailDTO = cartRemoteFacade.queryCheckedCartItemByUserId(orderCreateRequest.getCustomerId());
 List<CartItemDTO> cartItemList = shoppingCartDetailDTO.getCartItemDtoS();

 //校驗庫存
 checkInventory(cartItemList);
 ...
 
 transactionTemplate.executeWithoutResult(status -> {  
     orderRepository.save(tradeOrder);
  eventPublisher.publishEvent(new OrderCreatedEvent(tradeOrder)); 
 });

 return orderSn;
}

然而,這裡涉及到另一個問題:在儲存訂單後我們透過EventPublisher釋出了一個事件,讓監聽者來處理剩下的業務邏輯,(在Dailymart中建立訂單後需要進行庫存預扣),在預設情況下,Spring的事件監聽機制是同步的將程式碼進行解耦,我們希望庫存扣減如果出現失敗需要回滾訂單,而程式設計式事務無法控制監聽者的事務。因此,在這種場景下並不適合使用程式設計式事務來處理。

敲黑板:使用程式設計式事務替代宣告式事務是解決長事務最簡單的實現方式,在大部分場景下都可以採用。在使用時要注意程式設計式事務搭配EventPublisher時無法控制監聽者的事務。

對方法進行拆分

另外一種常見的處理措施就是將方法進行拆分,將大方法拆成小方法,將不需要事務管理的邏輯與事務操作拆開。

public String createOrder(OrderCreateRequest orderCreateRequest) {
 // 獲取購物車詳情
 ShoppingCartDetailDTO shoppingCartDetailDTO = cartRemoteFacade.queryCheckedCartItemByUserId(orderCreateRequest.getCustomerId());
 List<CartItemDTO> cartItemList = shoppingCartDetailDTO.getCartItemDtoS();

 //校驗庫存
 checkInventory(cartItemList);
 ...
 
 this.saveOrder(tradeOrder)

 return orderSn;
}

@Transactional(rollbackFor = RuntimeException.class)
private void saveOrder(TradeOrder tradeOrder)
{
 orderRepository.save(tradeOrder);
 eventPublisher.publishEvent(new OrderCreatedEvent(tradeOrder)); 
}

在上述程式碼中,獲取購物車詳情與庫存校驗不需要事務,將其與事務方法saveOrder()分開。然而,這樣的簡單拆分會導致事務不生效。這又涉及到另一個知識點:

@Transactional註解的宣告式事務是透過spring aop起作用的,而spring aop需要生成代理物件,直接在同一個類中方法呼叫使用的還是原始物件,事務不生效。其他幾個常見的事務不生效的場景為:

  • @Transactional 應用在非 public 修飾的方法上
  • @Transactional 註解屬性 propagation 設定錯誤
  • @Transactional 註解屬性 rollbackFor 設定錯誤
  • 同一個類中方法呼叫,導致@Transactional失效
  • 異常被catch捕獲導致@Transactional失效

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

  1. 將方法放入另一個類,如新增一個Manager層,透過Spring注入,這樣符合了在物件之間呼叫的條件。詳細說明可以參考我的文章為什麼阿里建議給MVC三層架構再加一層Manager層!。

  2. 啟動類新增@EnableAspectJAutoProxy(exposeProxy = true),方法內使用AopContext.currentProxy()獲得代理類,使用事務。

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



public String createOrder(OrderCreateRequest orderCreateRequest) {
 ...
 OrderService orderService = (OrderService)AopContext.currentProxy(); 
    orderService.saveData(tradeOrder);  
 return orderSn;
}

@Transactional(rollbackFor = RuntimeException.class)
private void saveOrder(TradeOrder tradeOrder)
{
 orderRepository.save(tradeOrder);
 eventPublisher.publishEvent(new OrderCreatedEvent(tradeOrder)); 
}

然而,Dailymart專案是基於DDD的分層架構模型實現。原來的業務邏輯是在應用服務編寫,在我們專案中只需要將儲存訂單的邏輯放在領域服務層,由領域服務保證事務,而應用服務層負責組裝業務邏輯。最終程式碼如下:

private final TradeOrderService tradeOrderService;

@Override  
// @Transactional(rollbackFor = RuntimeException.class)  
public String createOrder(OrderCreateRequest orderCreateRequest) {  
    // 生成訂單編號  
    String orderSn = IdUtils.nextIdStr();  
    // 獲取購物車詳情  
    ShoppingCartDetailDTO shoppingCartDetailDTO = cartRemoteFacade.queryCheckedCartItemByUserId(orderCreateRequest.getCustomerId());  
    List<CartItemDTO> cartItemList = shoppingCartDetailDTO.getCartItemDtoS();  
      
    // 校驗庫存  
    checkInventory(cartItemList);  
 ...
      
    tradeOrderService.save(tradeOrder); 
    return orderSn;  
}

@Service  
@RequiredArgsConstructor(onConstructor = @__(@Autowired))  
public class TradeOrderServiceImpl implements TradeOrderService {  
      
    private final ApplicationEventPublisher eventPublisher;  
      
    private final OrderRepository orderRepository;  
      
    @Override  
    @Transactional    
    public void save(TradeOrder tradeOrder) {  
        orderRepository.save(tradeOrder);  
        eventPublisher.publishEvent(new OrderCreatedEvent(tradeOrder));  
    }
}

小結

本文討論了長事務的危害及解決方案。首先,我們探討了長事務導致的問題,包括資料庫連線池耗盡、死鎖等。其次,介紹了兩種解決策略:採用程式設計式事務和對方法進行拆分。程式設計式事務提供了手動控制事務範圍的方式,但需要注意搭配EventPublisher可能導致監聽者事務無法控制的問題。對方法進行拆分是一種更通用的方法,能夠減小事務範圍,提高執行效率。最後,透過實際的DDD分層架構示例,展示了在應用服務層和領域服務層中如何組織業務邏輯,確保事務正確性和效能。


來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70024923/viewspace-3001509/,如需轉載,請註明出處,否則將追究法律責任。

相關文章