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 的默認實現(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)用的陷阱:一旦進入原始目標(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ù)在進入 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 后未拋出),代理對象將認為方法執(zhí)行成功,從而提交事務(wù)。
解決方案
- 避免吞噬異常:在
catch塊處理完日志或其他邏輯后,務(wù)必將異常再次拋出。 - 手動標(biāo)記回滾:如果業(yè)務(wù)邏輯要求不能拋出異常,則必須在
catch塊中手動標(biāo)記事務(wù)狀態(tài)為回滾:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
場景四:異常類型不匹配
問題描述
方法拋出了異常,但異常類型是檢查型異常(Checked Exception,如 IOException、SQLException),而 @Transactional 使用了默認配置。
示例代碼
@Service
public class OrderService {
@Transactional // 使用默認配置
public void createOrder() throws IOException {
insertOrder();
if (errorCondition) {
throw new IOException("IO Error");
}
}
}
原因分析
Spring 的 @Transactional 注解默認配置的 rollbackFor 屬性僅包含 RuntimeException 和 Error。這意味著,對于所有繼承自 Exception 但非 RuntimeException 的檢查型異常,Spring 默認不會觸發(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)使用默認的 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)文章
Spring Security內(nèi)存中認證的實現(xiàn)
本文主要介紹了Spring Security內(nèi)存中認證的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11
Java中json使用方法_動力節(jié)點Java學(xué)院整理
JSON(JavaScript Object Notation) 是一種輕量級的數(shù)據(jù)交換格式, json是個非常重要的數(shù)據(jù)結(jié)構(gòu),在web開發(fā)中應(yīng)用十分廣泛。下面通過本文給大家講解Java中json使用方法,感興趣的朋友一起看看吧2017-07-07
Java static 與 final關(guān)鍵字實例詳解
本文詳細介紹了Java中的static和final關(guān)鍵字,包括它們的本質(zhì)、內(nèi)存分配、線程安全問題以及在類加載過程中的內(nèi)存變化,通過舉例和解釋,感興趣的朋友跟隨小編一起看看吧2026-01-01
java實現(xiàn)word轉(zhuǎn)pdf or直接生成pdf文件
這篇文章主要介紹了java實現(xiàn)word轉(zhuǎn)pdf or直接生成pdf文件方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04
Java中Map接口使用以及有關(guān)集合的面試知識點匯總
在java面試過程中,Map時常會被作為一個面試點來問,下面這篇文章主要給大家介紹了關(guān)于Java中Map接口使用以及有關(guān)集合的面試知識點匯總的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-07-07
Java 如何實現(xiàn)POST(x-www-form-urlencoded)請求
這篇文章主要介紹了Java 實現(xiàn)POST(x-www-form-urlencoded)請求,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10

