Spring事務(wù)失效的問題及解決方案
Spring事務(wù)失效問題
開場
Spring事務(wù)管理,在Service實現(xiàn)類的方法上添加@Transactional注解就能進行事務(wù)控制,但是最近遇到一個事務(wù)失效的問題,淺淺分析一下
問題
在開發(fā)過程中發(fā)現(xiàn)這樣一個bug,有一段邏輯代碼:商品入庫、并且生成一個入庫單的操作;首先肯定的是需要操作兩張表,一是 入庫加庫存,二是 新增一個入庫單類似于入庫記錄。但是測試時候遇到一個問題,有一個商品入庫成功了,庫存也加進去了,但是沒有生成入庫單,我立馬想到的是難道是事務(wù)沒有控制好?翻看接口代碼發(fā)現(xiàn)還真是。。。
Spring事務(wù)失效的原因
我們都知道@Transactional失效的原因有多種,列舉一下,后面有時間再仔細分析研究
1.方法訪問修飾符:
- @Transactional注解只對public方法生效。
- 如果事務(wù)性方法是private、protected,事務(wù)不會生效
2.類內(nèi)部方法調(diào)用:
- 在同一個類中調(diào)用標(biāo)注了**
@Transactional**的方法(即自調(diào)用)時,事務(wù)管理器不會介入,因為Spring AOP代理無法攔截內(nèi)部調(diào)用
3.沒有啟用事務(wù)管理:
- 必須顯式啟用Spring的事務(wù)管理,通常使用**@EnableTransactionManagement**注解在配置類上,或者在XML中配置使用
<tx:annotation-driven/>
4.事務(wù)傳播屬性不當(dāng):
- 事務(wù)傳播屬性(Propagetion)配置不當(dāng)會導(dǎo)致事務(wù)失效。
- 例如:傳播行為
REQUIRES_NEW會掛起當(dāng)前事務(wù),創(chuàng)建一個新事物,這可能不是我們想要的
5.異常處理不當(dāng):
- 默認情況下,Spring事務(wù)管理只在運行時異常(RuntimeExpection及其子類)和錯誤(Error及其子類)時回滾。
- 如果捕獲并處理了一場,但沒有重新拋出,事務(wù)不會回滾。此外需要注意,對于檢查異常(Exception及其子類),需要制定rollbackFor屬性來觸發(fā)事務(wù)回滾,例如:
@Transactional(rollbackFor = Exception.class)
6.多線程環(huán)境:
- Transactional 注解無法再多線程環(huán)境中傳播事務(wù)。
- Spring事務(wù)管理依賴于線程局部變量(ThreadLocal),在不同獻策會給你之間共享事務(wù)需要顯式處理。(后面遇到的話仔細研究一下)
7.數(shù)據(jù)源和事務(wù)管理器配置不一致:
- 確保數(shù)據(jù)源(DataSource)和事務(wù)管理器(TransactionManager)配置一致。
- 如果有多個數(shù)據(jù)源和事務(wù)管理器,需要明確指定使用的事務(wù)管理器。
8.Spring代理模式限制:
- Spring默認使用AOP代理來管理事務(wù)。
- 默認情況下使用JDK動態(tài)代理,只有實現(xiàn)接口的類可以使用事務(wù)。
- 如果沒有實現(xiàn)接口,需要使用CGLIB代理,通過proxyTargetClass=true啟用CGLIB代理。
9.異步方法:
- 一步方法(如使用@Async注解的方法)運行在獨立線程中,不會參與當(dāng)前線程的事務(wù)管理。
- 需要特別處理異步方法的事務(wù)管理。
排查bug
言歸正傳,我在這段邏輯代碼中發(fā)現(xiàn)有兩個問題,首先代碼塊被 try catch 捕獲后沒有重新拋出異常,而且這段代碼在catch 里面有日志記錄,插入到一張日志表;業(yè)務(wù)邏輯太多此處就不貼真實代碼了,但是我寫了一個demo是可以復(fù)刻問題的。
代碼如下:
@Transactional(rollbackFor = Exception.class)
public User update(User userDto) {
try {
//查詢用戶
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StrUtil.isNotBlank(userDto.getId()), User::getId, userDto.getId());
User user = userDao.selectOne(queryWrapper);
//修改數(shù)據(jù):扣錢
user.setMoney(user.getMoney().subtract(userDto.getMoney()));
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(StrUtil.isNotBlank(userDto.getId()), User::getId, userDto.getId());
userDao.update(user);
//模擬異常
int i = 1/0;
} catch (Exception e) {
log.error(e.getMessage(),e);
//日志記錄:將錯誤信息插入到日志表
insertLog(userDto, e);
}
return null;
}
/*
* 日志記錄的方法
*/
public void recordLog(Exception e) {
LogHistory logHistory = new LogHistory();
logHistory.setCode("1");
logHistory.setTime(new Date());
logHistory.setErrorMsg(e.getMessage());
logHistoryMapper.insert(logHistory);
}
上面這段代碼,大家還有發(fā)現(xiàn)問題其他嗎,細心的小伙伴一定還能發(fā)現(xiàn)其他問題,那就是這個 catch 捕獲異常后并沒有拋出,所以這段代碼并不會回滾,但是問題來了,如果我們在 catch 中最后拋出異常,那么日志記錄的操作也將回滾,這里就會有點沖突,如果代碼有異常報錯,我們的目的就是要回滾,但是還不能把日志記錄給回滾。
解決方案
發(fā)現(xiàn)了問題,該如何去解決呢?
在這里我用到了一個事務(wù)的傳播行為:先說結(jié)論,將事務(wù)的傳播屬性設(shè)置為REQUIRES_NEW,就是在當(dāng)前事務(wù)中新建了一個事務(wù),用新的事務(wù)去控制我日志記錄的操作,這樣的話兩個事務(wù)互不影響,A事務(wù)如果拋出異常,不影響B(tài)事務(wù),B事務(wù)正常插入數(shù)據(jù),完美解決!當(dāng)然不止這一種解決方案,還有異步調(diào)用,mq發(fā)消息等方式。
但是需要注意,我們需要在單獨的Service實現(xiàn)類中去編寫日志記錄的方法,不能在A事務(wù)中去編寫了,代碼如下:
@Transactional(rollbackFor = Exception.class)
public User update(User userDto) {
try {
//查詢用戶
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StrUtil.isNotBlank(userDto.getId()), User::getId, userDto.getId());
User user = userDao.selectOne(queryWrapper);
//修改數(shù)據(jù):扣錢
user.setMoney(user.getMoney().subtract(userDto.getMoney()));
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(StrUtil.isNotBlank(userDto.getId()), User::getId, userDto.getId());
userDao.update(user);
//模擬異常
int i = 1/0;
} catch (Exception e) {
log.error(e.getMessage(),e);
//日志記錄:需要在另外的service中編寫
logHistoryService.insertLog(userDto, e);
//拋出異常
throw e;
}
return null;
}
/*
* 日志記錄
* 此處我們需要設(shè)置事務(wù)的傳播屬性為 REQUIRES_NEW
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public LogHistory insertLog(User userDto, Exception e) {
LogHistory logHistory = new LogHistory();
logHistory.setCode("111");
logHistory.setTime(new Date());
logHistory.setErrorMsg(e.getMessage());
logHistoryMapper.insert(logHistory);
return logHistory;
}
事務(wù)的傳播屬性
在 Spring 中,事務(wù)的傳播屬性(Propagation)決定了事務(wù)的行為在方法間調(diào)用時的傳播方式。傳播屬性通過 @Transactional 注解的 propagation 屬性來設(shè)置,Spring 提供了以下幾種傳播屬性:
1.REQUIRED(默認值):
- 如果當(dāng)前已經(jīng)存在一個事務(wù),則加入該事務(wù)。
- 如果當(dāng)前沒有事務(wù),則創(chuàng)建一個新的事務(wù)。
- 這是最常見的傳播行為。
2.REQUIRES_NEW:
- 無論當(dāng)前是否存在事務(wù),總是創(chuàng)建一個新的事務(wù)。
- 如果當(dāng)前存在事務(wù),則掛起當(dāng)前事務(wù),直到新事務(wù)完成。
3.SUPPORTS:
- 如果當(dāng)前存在事務(wù),則加入該事務(wù)。
- 如果當(dāng)前沒有事務(wù),則以非事務(wù)方式執(zhí)行。
4.NOT_SUPPORTED:
- 以非事務(wù)方式執(zhí)行操作。
- 如果當(dāng)前存在事務(wù),則掛起當(dāng)前事務(wù),直到當(dāng)前操作完成。
5.MANDATORY:
- 必須在一個現(xiàn)有事務(wù)中運行。
- 如果當(dāng)前沒有事務(wù),則拋出異常。
6.NEVER:
- 必須在非事務(wù)上下文中運行。
- 如果當(dāng)前存在事務(wù),則拋出異常。
7.NESTED:
- 如果當(dāng)前存在事務(wù),則在嵌套事務(wù)中運行。
- 如果當(dāng)前沒有事務(wù),則創(chuàng)建一個新的事務(wù)。
- 嵌套事務(wù)使用保存點(savepoint),如果嵌套事務(wù)回滾,它只回滾到保存點,外部事務(wù)可以繼續(xù)。
- 如果當(dāng)前沒有事務(wù),則拋出異常。
8.NEVER:
- 必須在非事務(wù)上下文中運行。
- 如果當(dāng)前存在事務(wù),則拋出異常。
9.NESTED:
- 如果當(dāng)前存在事務(wù),則在嵌套事務(wù)中運行。
- 如果當(dāng)前沒有事務(wù),則創(chuàng)建一個新的事務(wù)。
- 嵌套事務(wù)使用保存點(savepoint),如果嵌套事務(wù)回滾,它只回滾到保存點,外部事務(wù)可以繼續(xù)。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
java基于quasar實現(xiàn)協(xié)程池的方法示例
本文主要介紹了java基于quasar實現(xiàn)協(xié)程池的方法示例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧<BR>2022-06-06
Java Arrays.sort()如何實現(xiàn)對int類型數(shù)組倒序排序
這篇文章主要介紹了Java Arrays.sort()如何實現(xiàn)對int類型數(shù)組倒序排序問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08
Maven中錯誤使用parent.relativePath導(dǎo)致構(gòu)建失敗問題
這篇文章主要介紹了Maven中錯誤使用parent.relativePath導(dǎo)致構(gòu)建失敗問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08
探討:使用httpClient在客戶端與服務(wù)器端傳輸對象參數(shù)的詳解
本篇文章是對使用httpClient在客戶端與服務(wù)器端傳輸對象參數(shù)進行了詳細的分析介紹,需要的朋友參考下2013-06-06
鎖超時發(fā)現(xiàn)parallelStream并行流線程上下文坑解決
這篇文章主要為大家介紹了鎖超時發(fā)現(xiàn)parallelStream并行流線程上下文坑解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08
java.lang.OutOfMemoryError: Java heap space錯誤
本文主要介紹了java.lang.OutOfMemoryError: Java heap space錯誤的問題解決,包括內(nèi)存泄漏、數(shù)據(jù)過大和JVM堆大小配置不足,提供了解決方法,具有一定的參考價值,感興趣的可以了解一下2025-03-03

