Spring事務長了個腿?輕鬆掌握技巧告別長事務煩惱!
來源:JAVA日知錄
大家好,我是飄渺。今天繼續DDD&微服務專欄。
在之前的文章 基於DDD的訂單建立 流程中,我們留下了一個問題:在createOrder()
方法中,我將呼叫遠端介面獲取購物車詳情、遠端庫存校驗、訂單儲存放在一個事務中,顯然這並不是一個正確的做法,因為它會導致長事務,今天就讓我們來解決這個問題。
為什麼會產生長事務
首先,讓我們來分析一下產生長事務的原因。
在Spring中,@Transactional
註解是基於AOP實現的,本質上是在目標方法執行前後進行攔截。在目標方法執行前加入或建立一個事務,在方法執行後,根據實際情況選擇提交或回滾事務。
當Spring遇到該註解時,會自動從資料庫連線池中獲取連線並開啟事務,然後繫結到ThreadLocal上,對於@Transactional
註解包裹的整個方法都是使用同一個連線。如果出現耗時的操作,如第三方介面呼叫、業務邏輯複雜、大批次資料處理等,就會導致佔用連線的時間很長,資料庫連線一直被佔用不釋放。一旦類似操作過多,就會導致資料庫連線池耗盡。
在開頭的例項中,一個事務中執行RPC操作是典型的長事務問題。類似的操作還包括在事務中進行大量資料查詢、業務規則處理等。
長事務會產生哪些問題
長事務引發的常見危害有:
資料庫連線池被佔滿,應用無法獲取連線資源; 容易引發資料庫死鎖; 資料庫回滾時間長; 在主從架構中會導致主從延時變大。
如何避免長事務
既然知道了長事務的危害,那麼在開發中如何避免這個問題呢?
很明顯,解決長事務的宗旨就是 對事務方法進行拆分,儘量讓事務變小,變快,減小事務的顆粒度。
程式設計式事務
因此,我們可以採用程式設計式事務替代宣告式事務@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失效
正確的拆分方法應該使用下面兩種:
將方法放入另一個類,如新增一個
Manager層
,透過Spring注入,這樣符合了在物件之間呼叫的條件。詳細說明可以參考我的文章為什麼阿里建議給MVC三層架構再加一層Manager層!。啟動類新增
@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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- SQL Server 查出未提交事務(長事務)SQLSQLServer
- 什麼是事務、事務特性、事務隔離級別、spring事務傳播特性?Spring
- STREAMS筆記(9) 大事務 & 長事務筆記
- 分散式事務之Spring事務與JMS事務(二)分散式Spring
- INFORMIX長事務及onstat工具用法ORM
- 【SQL】長事務診斷指令碼SQL指令碼
- spring 事務Spring
- spring事務Spring
- 用PHP輕鬆完成一個分散式事務TCC,保姆級教程PHP分散式
- 用Go輕鬆完成一個分散式事務TCC,保姆級教程Go分散式
- 用Java輕鬆完成一個分散式事務TCC,保姆級教程Java分散式
- java spring巢狀事務詳情和事務傳播型別JavaSpring巢狀型別
- 用Go輕鬆完成一個SAGA分散式事務,保姆級教程Go分散式
- 用python輕鬆完成一個分散式事務TCC,保姆級教程Python分散式
- Spring 事務管理Spring
- Spring配置事務Spring
- Spring事務管理Spring
- Spring事務配置的五種方式和spring裡面事務的傳播屬性和事務隔離級別Spring
- Spring的事務管理(二)宣告式事務管理Spring
- PostgreSQL DBA(27) - MVCC#7(避免長事務)SQLMVCC#
- goldengate中長事務引起的問題Go
- 本地事務和分散式事務的區別分散式
- 帶你十天輕鬆搞定 Go 微服務之大結局(分散式事務)Go微服務分散式
- Spring事務專題(三)事務的基本概念,Mysql事務處理原理SpringMySql
- @Transactional spring 配置事務 注意事項Spring
- Spring事務專題(四)Spring中事務的使用、抽象機制及模擬Spring事務實現Spring抽象
- Spring的事務管理(一) Spring事務管理的實現,事務的屬性(隔離級別,傳播行為,只讀)Spring
- long長事務回滾的模擬與定位
- 停止OGG e程式時遭遇長事務分析
- Spring系列.事務管理Spring
- Spring事務筆記Spring筆記
- Spring系列-事務管理Spring
- Spring-AOP事務Spring
- Spring的事務管理Spring
- spring事務不回滾Spring
- spring事務相關Spring
- Spring Webflux與事務SpringWebUX
- spring-5-事務Spring