Spring事務(wù)失效的6種常見典型場景分析與解決方案
前言
在 Spring 應(yīng)用程序開發(fā)中,聲明式事務(wù)(@Transactional)是保證數(shù)據(jù)一致性的核心機制。然而,在實際應(yīng)用中,開發(fā)者常遇到注解已添加但事務(wù)未回滾的情況。本文基于最近在領(lǐng)券業(yè)務(wù)中遇到的并發(fā)與事務(wù)沖突問題,深入分析 Spring 事務(wù)失效的六種典型場景,并提供相應(yīng)的解決方案。
場景一:事務(wù)方法訪問權(quán)限非 public
問題描述
將 @Transactional 注解應(yīng)用于非 public 修飾的方法(如 protected、private 或包級私有方法)時,事務(wù)將失效。
示例代碼
@Service
public class UserService {
@Transactional
protected void updateUser(User user) {
// 業(yè)務(wù)邏輯
}
}
原因分析
Spring 的聲明式事務(wù)依賴于 AOP。Spring AOP 的默認(rèn)實現(xiàn)(無論是 JDK 動態(tài)代理還是 CGLIB)通常要求目標(biāo)方法必須是public,以便代理對象能夠正確攔截并增強該方法。
此外,Spring 源碼中的 AbstractFallbackTransactionAttributeSource.computeTransactionAttribute 方法顯式規(guī)定了僅處理 public 方法:
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null; // 非 public 方法返回 null,不應(yīng)用事務(wù)配置
}
解決方案
確保被 @Transactional 注解修飾的方法訪問權(quán)限為 public。
場景二:同一類內(nèi)部方法自調(diào)用
這是開發(fā)中最易被忽視的失效原因。
問題描述
在一個沒有事務(wù)注解的普通方法內(nèi)部,直接調(diào)用同一類中被 @Transactional 注解修飾的方法,事務(wù)將失效。
示例代碼
@Service
public class UserCouponServiceImpl implements IUserCouponService {
// 入口方法,無事務(wù)注解,但在內(nèi)部調(diào)用了事務(wù)方法
@Override
public void receiveCoupon(Long couponId) {
// ... 前置校驗
// 【關(guān)鍵點】內(nèi)部直接調(diào)用事務(wù)方法,事務(wù)失效
this.saveUserCouponAndUpdateCoupon(coupon, userId);
}
@Transactional
public void saveUserCouponAndUpdateCoupon(Coupon coupon, Long userId){
// 數(shù)據(jù)庫操作:扣減庫存、保存記錄
couponMapper.plusIssueNum(couponId);
save(userCoupon);
}
}
原因分析:為什么this調(diào)用會導(dǎo)致事務(wù)失效?
Spring 的聲明式事務(wù)是基于 AOP 和 動態(tài)代理 實現(xiàn)的。
- 代理對象的攔截機制:當(dāng) Spring 容器啟動時,會為使用了
@Transactional的 Bean 創(chuàng)建一個代理對象。這個代理對象持有一個指向原始目標(biāo)對象(Target,即UserCouponServiceImpl實例)的引用。 - 外部調(diào)用的流程:當(dāng) Controller 調(diào)用
userCouponService.receiveCoupon(...)時,實際上是在調(diào)用代理對象的方法。代理對象會檢查該方法是否有@Transactional注解。- 如果有,代理對象會在調(diào)用目標(biāo)方法前開啟事務(wù),調(diào)用后提交事務(wù)。
- 如果沒有(如
receiveCoupon),代理對象會直接將請求轉(zhuǎn)發(fā)給原始目標(biāo)對象。
- 內(nèi)部調(diào)用的陷阱:一旦進(jìn)入原始目標(biāo)對象的方法內(nèi)部(如
receiveCoupon執(zhí)行中),代碼執(zhí)行流就已經(jīng)脫離了代理對象的控制。此時,代碼中直接調(diào)用的saveUserCouponAndUpdateCoupon(...)等同于this.saveUserCouponAndUpdateCoupon(...)。這里的this指向的是原始目標(biāo)對象本身,而不是代理對象。 - 結(jié)論:由于繞過了代理對象直接使用service對象,Spring 的事務(wù)攔截器無法介入,最終導(dǎo)致AOP實現(xiàn)的事務(wù)邏輯根本沒有執(zhí)行。
解決方案
方案 A:直接給入口方法添加事務(wù)注解(常規(guī)解法)
最簡單的解決方法是給入口方法 receiveCoupon 也添加 @Transactional 注解。這樣,事務(wù)在進(jìn)入 receiveCoupon 時就已經(jīng)開啟,后續(xù)的調(diào)用都在同一個事務(wù)上下文中運行。
@Transactional // 簡單粗暴,直接加事務(wù)
public void receiveCoupon(Long couponId) {
saveUserCouponAndUpdateCoupon(coupon, userId);
}
但是,在并發(fā)場景下,這種方案往往不可行。
方案 B:強制使用代理對象調(diào)用(高并發(fā)場景推薦)
在我的領(lǐng)券業(yè)務(wù)中,為了防止超賣,我們需要使用 synchronized 鎖來控制并發(fā)。
- 如果使用方案 A:事務(wù)包裹了鎖(Transaction 包含 synchronized)。
- 執(zhí)行順序:
開啟事務(wù) -> 加鎖 -> 執(zhí)行業(yè)務(wù) -> 解鎖 -> 提交事務(wù)。 - 風(fēng)險:線程 A 解鎖后,事務(wù)尚未提交。此時線程 B 獲取鎖并讀取數(shù)據(jù),讀到的仍然是舊數(shù)據(jù)(因為線程 A 的事務(wù)還沒提交),導(dǎo)致鎖失效,發(fā)生超賣。
- 執(zhí)行順序:
- 為了解決鎖失效:我們必須保證鎖的范圍大于事務(wù)(synchronized 包含 Transaction)。
- 執(zhí)行順序:
加鎖 -> 開啟事務(wù) -> 執(zhí)行業(yè)務(wù) -> 提交事務(wù) -> 解鎖。 - 這就要求
receiveCoupon方法不能加事務(wù)注解(它是加鎖的地方),只有內(nèi)部調(diào)用的saveUserCouponAndUpdateCoupon方法需要加事務(wù)。
- 執(zhí)行順序:
這就回到了最初的問題:內(nèi)部調(diào)用會導(dǎo)致事務(wù)失效。
為了同時滿足“鎖包事務(wù)”和“事務(wù)生效”兩個條件,我們必須在 receiveCoupon 內(nèi)部,手動獲取當(dāng)前的代理對象來調(diào)用事務(wù)方法,強行讓調(diào)用邏輯重新經(jīng)過 Spring AOP 的攔截器鏈。
實現(xiàn)步驟:
引入 AspectJ 依賴:
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>開啟代理暴露(在啟動類或配置類):
@EnableAspectJAutoProxy(exposeProxy = true)
使用 AopContext 獲取代理對象并調(diào)用:
參考 UserCouponServiceImpl.java 中的實現(xiàn):public void receiveCoupon(Long couponId) { // ... 省略校驗邏輯 synchronized (userId.toString().intern()) { // 【核心代碼】從 ThreadLocal 中獲取當(dāng)前 AOP 代理對象 IUserCouponService proxy = (IUserCouponService) AopContext.currentProxy(); // 通過代理對象調(diào)用,觸發(fā)事務(wù)切面邏輯 proxy.saveUserCouponAndUpdateCoupon(coupon, userId); } }
通過這種在service調(diào)用的方法使用代理對象調(diào)用的方式,我們既控制了事務(wù)的粒度(只包裹核心數(shù)據(jù)庫操作),又避免了內(nèi)部調(diào)用繞過代理機制導(dǎo)致的事務(wù)失效,完美解決了高并發(fā)下的數(shù)據(jù)一致性問題。
場景三:事務(wù)方法內(nèi)部捕獲異常且未拋出
問題描述
在事務(wù)方法內(nèi)部使用 try-catch 塊捕獲了異常,且在 catch 塊中未再次拋出異常,導(dǎo)致事務(wù)提交而非回滾。
示例代碼
@Service
public class OrderService {
@Transactional
public void createOrder() {
try {
insertOrder();
reduceStock(); // 假設(shè)此處拋出異常
} catch (Exception e) {
e.printStackTrace();
// 異常被吞噬,未向外拋出
}
}
}
原因分析
Spring AOP 代理對象在調(diào)用目標(biāo)方法后,會檢查方法執(zhí)行過程中是否拋出了異常。
- 如果捕獲到異常,且異常類型符合回滾規(guī)則,則執(zhí)行回滾。
- 如果目標(biāo)方法內(nèi)部自行處理了異常(即 catch 后未拋出),代理對象將認(rèn)為方法執(zhí)行成功,從而提交事務(wù)。
解決方案
- 避免吞噬異常:在
catch塊處理完日志或其他邏輯后,務(wù)必將異常再次拋出。 - 手動標(biāo)記回滾:如果業(yè)務(wù)邏輯要求不能拋出異常,則必須在
catch塊中手動標(biāo)記事務(wù)狀態(tài)為回滾:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
場景四:異常類型不匹配
問題描述
方法拋出了異常,但異常類型是檢查型異常(Checked Exception,如 IOException、SQLException),而 @Transactional 使用了默認(rèn)配置。
示例代碼
@Service
public class OrderService {
@Transactional // 使用默認(rèn)配置
public void createOrder() throws IOException {
insertOrder();
if (errorCondition) {
throw new IOException("IO Error");
}
}
}
原因分析
Spring 的 @Transactional 注解默認(rèn)配置的 rollbackFor 屬性僅包含 RuntimeException 和 Error。這意味著,對于所有繼承自 Exception 但非 RuntimeException 的檢查型異常,Spring 默認(rèn)不會觸發(fā)回滾。
解決方案
顯式配置 rollbackFor 屬性,建議指定為 Exception.class 以覆蓋所有異常類型:
@Transactional(rollbackFor = Exception.class)
場景五:事務(wù)傳播行為配置錯誤
問題描述
在嵌套事務(wù)場景中,內(nèi)部方法的傳播行為配置導(dǎo)致其事務(wù)獨立于外部事務(wù),從而破壞了整體原子性。
示例代碼
@Service
public class OrderService {
@Transactional
public void createOrder(){
insertOrder();
try {
stockService.reduceStock(); // 即使外部回滾,此方法可能已提交
} catch (Exception e) {
// ...
}
throw new RuntimeException("Error");
}
}
@Service
public class StockService {
// REQUIRES_NEW 開啟獨立事務(wù)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void reduceStock() {
// ...
}
}
原因分析
Propagation.REQUIRES_NEW 策略會掛起當(dāng)前事務(wù),并開啟一個新的物理事務(wù)。即使外部調(diào)用方(createOrder)后續(xù)發(fā)生異常并回滾,reduceStock 方法的獨立事務(wù)一旦提交,其數(shù)據(jù)變更將永久生效,導(dǎo)致數(shù)據(jù)不一致。
解決方案
根據(jù)業(yè)務(wù)一致性需求正確選擇傳播行為。對于大多數(shù)需要保證原子性的組合操作,應(yīng)使用默認(rèn)的 Propagation.REQUIRED,確保所有方法在同一個邏輯事務(wù)中運行。
場景六:類未被 Spring 容器管理
問題描述
調(diào)用 @Transactional 方法的對象實例并非由 Spring 容器創(chuàng)建和管理。
示例代碼
// 缺少 @Service 或 @Component 注解
public class OrderService {
@Transactional
public void createOrder() {
// ...
}
}
或者:
OrderService service = new OrderService(); // 手動 new 實例 service.createOrder();
原因分析
Spring 的聲明式事務(wù)完全依賴于 IoC 容器對 Bean 的生命周期管理和 AOP 代理生成。如果一個類沒有被注冊為 Spring Bean(缺少 @Service、@Component 等注解),或者對象是通過 new 關(guān)鍵字手動實例化的,Spring 容器無法感知該對象,也就無法為其創(chuàng)建代理并織入事務(wù)切面邏輯。
解決方案
- 確保業(yè)務(wù)類上添加了
@Service、@Component等組件注解。 - 在其他組件中使用該類時,必須通過依賴注入(
@Autowired或構(gòu)造器注入)獲取實例,嚴(yán)禁手動實例化。
總結(jié)
Spring 事務(wù)失效問題通常源于對 Spring AOP 代理機制理解的偏差。在排查此類問題時,應(yīng)重點關(guān)注以下三個維度:
- 代理機制:是否存在對象自調(diào)用、類是否被容器管理、方法可見性是否合規(guī)。
- 異常處理:異常是否被捕獲吞噬、異常類型是否在回滾范圍內(nèi)。
- 事務(wù)配置:傳播行為是否符合業(yè)務(wù)預(yù)期。
在我的項目領(lǐng)券業(yè)務(wù)場景中,我們?yōu)榱思骖櫜l(fā)控制(synchronized)和事務(wù)原子性,采用了手動獲取代理對象(AopContext.currentProxy())的方案,有效解決了自調(diào)用導(dǎo)致的事務(wù)失效問題。
到此這篇關(guān)于Spring事務(wù)失效的6種常見典型場景分析與解決方案的文章就介紹到這了,更多相關(guān)Spring事務(wù)失效解決內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Java中Dijkstra(迪杰斯特拉)算法的圖解與實現(xiàn)
Dijkstra(迪杰斯特拉)算法是典型的單源最短路徑算法,用于計算一個節(jié)點到其他所有節(jié)點的最短路徑。本文將詳解該算法的圖解與實現(xiàn),需要的可以參考一下2022-05-05
SpringBoot分布式WebSocket的實現(xiàn)指南
在現(xiàn)代Web應(yīng)用中,實時通信已成為基本需求,而WebSocket是實現(xiàn)這一功能的核心技術(shù),本文將詳細(xì)介紹如何在Spring Boot項目中實現(xiàn)分布式WebSocket,包括完整的技術(shù)方案、實現(xiàn)步驟和核心代碼,需要的朋友可以參考下2025-10-10
Spring實戰(zhàn)之設(shè)置普通屬性值的方法示例
這篇文章主要介紹了Spring實戰(zhàn)之設(shè)置普通屬性值的方法,結(jié)合實例形式分析了Spring設(shè)置普通屬性值的方法及相關(guān)操作注意事項,需要的朋友可以參考下2019-11-11
SpringBoot結(jié)合Redis實現(xiàn)會話管理功能
在Web應(yīng)用程序中,會話管理是一項重要的任務(wù),它涉及跟蹤用戶的身份驗證狀態(tài)和其他相關(guān)信息,以確保用戶在與應(yīng)用程序交互時的連續(xù)性和安全性,在本文中,我們將探討如何使用Spring Boot和Redis實現(xiàn)高效的會話管理2023-06-06
一文帶你了解Java創(chuàng)建型設(shè)計模式之原型模式
原型模式其實就是從一個對象在創(chuàng)建另外一個可定制的對象,不需要知道任何創(chuàng)建的細(xì)節(jié)。本文就來通過示例為大家詳細(xì)聊聊原型模式,需要的可以參考一下2022-09-09
Java實戰(zhàn)房屋租賃網(wǎng)的實現(xiàn)流程
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+jsp+mysql+maven實現(xiàn)一個房屋租賃網(wǎng)站,大家可以在過程中查缺補漏,提升水平2021-11-11
Java泛型枚舉Annotation接口詳細(xì)解讀與Eclipse發(fā)展
這篇文章主要給大家介紹了關(guān)于Java中方法使用的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-08-08

