新聞中心
大家好,我是飄渺。今天繼續(xù)DDD&微服務(wù)專欄。

讓客戶滿意是我們工作的目標,不斷超越客戶的期望值來自于我們對這個行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價值的長期合作伙伴,公司提供的服務(wù)項目有:域名申請、網(wǎng)絡(luò)空間、營銷軟件、網(wǎng)站建設(shè)、洋縣網(wǎng)站維護、網(wǎng)站推廣。
在之前的文章 基于DDD的訂單創(chuàng)建 流程中,我們留下了一個問題:在createOrder()方法中,我將調(diào)用遠程接口獲取購物車詳情、遠程庫存校驗、訂單保存放在一個事務(wù)中,顯然這并不是一個正確的做法,因為它會導致長事務(wù),今天就讓我們來解決這個問題。
圖片
為什么會產(chǎn)生長事務(wù)
首先,讓我們來分析一下產(chǎn)生長事務(wù)的原因。
在Spring中,@Transactional注解是基于AOP實現(xiàn)的,本質(zhì)上是在目標方法執(zhí)行前后進行攔截。在目標方法執(zhí)行前加入或創(chuàng)建一個事務(wù),在方法執(zhí)行后,根據(jù)實際情況選擇提交或回滾事務(wù)。
當Spring遇到該注解時,會自動從數(shù)據(jù)庫連接池中獲取連接并開啟事務(wù),然后綁定到ThreadLocal上,對于@Transactional注解包裹的整個方法都是使用同一個連接。如果出現(xiàn)耗時的操作,如第三方接口調(diào)用、業(yè)務(wù)邏輯復雜、大批量數(shù)據(jù)處理等,就會導致占用連接的時間很長,數(shù)據(jù)庫連接一直被占用不釋放。一旦類似操作過多,就會導致數(shù)據(jù)庫連接池耗盡。
在開頭的實例中,一個事務(wù)中執(zhí)行RPC操作是典型的長事務(wù)問題。類似的操作還包括在事務(wù)中進行大量數(shù)據(jù)查詢、業(yè)務(wù)規(guī)則處理等。
長事務(wù)會產(chǎn)生哪些問題
長事務(wù)引發(fā)的常見危害有:
- 數(shù)據(jù)庫連接池被占滿,應(yīng)用無法獲取連接資源;
- 容易引發(fā)數(shù)據(jù)庫死鎖;
- 數(shù)據(jù)庫回滾時間長;
- 在主從架構(gòu)中會導致主從延時變大。
如何避免長事務(wù)
既然知道了長事務(wù)的危害,那么在開發(fā)中如何避免這個問題呢?
很明顯,解決長事務(wù)的宗旨就是 對事務(wù)方法進行拆分,盡量讓事務(wù)變小,變快,減小事務(wù)的顆粒度。
編程式事務(wù)
因此,我們可以采用編程式事務(wù)替代聲明式事務(wù)@Transactional。在Spring項目中,可以注入TransactionTemplate對象,然后手動控制事務(wù)范圍。改造過后的代碼如下所示:
public String createOrder(OrderCreateRequest orderCreateRequest) {
// 獲取購物車詳情
ShoppingCartDetailDTO shoppingCartDetailDTO = cartRemoteFacade.queryCheckedCartItemByUserId(orderCreateRequest.getCustomerId());
List cartItemList = shoppingCartDetailDTO.getCartItemDtoS();
//校驗庫存
checkInventory(cartItemList);
...
transactionTemplate.executeWithoutResult(status -> {
orderRepository.save(tradeOrder);
eventPublisher.publishEvent(new OrderCreatedEvent(tradeOrder));
});
return orderSn;
} 然而,這里涉及到另一個問題:在保存訂單后我們通過EventPublisher發(fā)布了一個事件,讓監(jiān)聽者來處理剩下的業(yè)務(wù)邏輯,(在Dailymart中創(chuàng)建訂單后需要進行庫存預扣),在默認情況下,Spring的事件監(jiān)聽機制是同步的將代碼進行解耦,我們希望庫存扣減如果出現(xiàn)失敗需要回滾訂單,而編程式事務(wù)無法控制監(jiān)聽者的事務(wù)。因此,在這種場景下并不適合使用編程式事務(wù)來處理。
敲黑板:使用編程式事務(wù)替代聲明式事務(wù)是解決長事務(wù)最簡單的實現(xiàn)方式,在大部分場景下都可以采用。在使用時要注意編程式事務(wù)搭配EventPublisher時無法控制監(jiān)聽者的事務(wù)。
對方法進行拆分
另外一種常見的處理措施就是將方法進行拆分,將大方法拆成小方法,將不需要事務(wù)管理的邏輯與事務(wù)操作拆開。
public String createOrder(OrderCreateRequest orderCreateRequest) {
// 獲取購物車詳情
ShoppingCartDetailDTO shoppingCartDetailDTO = cartRemoteFacade.queryCheckedCartItemByUserId(orderCreateRequest.getCustomerId());
List 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));
} 在上述代碼中,獲取購物車詳情與庫存校驗不需要事務(wù),將其與事務(wù)方法saveOrder()分開。然而,這樣的簡單拆分會導致事務(wù)不生效。這又涉及到另一個知識點:
@Transactional注解的聲明式事務(wù)是通過spring aop起作用的,而spring aop需要生成代理對象,直接在同一個類中方法調(diào)用使用的還是原始對象,事務(wù)不生效。其他幾個常見的事務(wù)不生效的場景為:
- @Transactional 應(yīng)用在非 public 修飾的方法上
- @Transactional 注解屬性 propagation 設(shè)置錯誤
- @Transactional 注解屬性 rollbackFor 設(shè)置錯誤
- 同一個類中方法調(diào)用,導致@Transactional失效
- 異常被catch捕獲導致@Transactional失效
正確的拆分方法應(yīng)該使用下面兩種:
- 將方法放入另一個類,如新增一個Manager層,通過Spring注入,這樣符合了在對象之間調(diào)用的條件。詳細說明可以參考我的文章為什么阿里建議給MVC三層架構(gòu)再加一層Manager層!。
- 啟動類添加@EnableAspectJAutoProxy(exposeProxy = true),方法內(nèi)使用AopContext.currentProxy()獲得代理類,使用事務(wù)。
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的分層架構(gòu)模型實現(xiàn)。原來的業(yè)務(wù)邏輯是在應(yīng)用服務(wù)編寫,在我們項目中只需要將保存訂單的邏輯放在領(lǐng)域服務(wù)層,由領(lǐng)域服務(wù)保證事務(wù),而應(yīng)用服務(wù)層負責組裝業(yè)務(wù)邏輯。最終代碼如下:
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 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));
}
} 小結(jié)
本文討論了長事務(wù)的危害及解決方案。首先,我們探討了長事務(wù)導致的問題,包括數(shù)據(jù)庫連接池耗盡、死鎖等。其次,介紹了兩種解決策略:采用編程式事務(wù)和對方法進行拆分。編程式事務(wù)提供了手動控制事務(wù)范圍的方式,但需要注意搭配EventPublisher可能導致監(jiān)聽者事務(wù)無法控制的問題。對方法進行拆分是一種更通用的方法,能夠減小事務(wù)范圍,提高執(zhí)行效率。最后,通過實際的DDD分層架構(gòu)示例,展示了在應(yīng)用服務(wù)層和領(lǐng)域服務(wù)層中如何組織業(yè)務(wù)邏輯,確保事務(wù)正確性和性能。
分享題目:Spring事務(wù)長了個腿?輕松掌握技巧告別長事務(wù)煩惱!
分享路徑:http://www.5511xx.com/article/cdejcci.html


咨詢
建站咨詢
