Spring事務(wù)失效的問題及解決方案
Spring事務(wù)失效問題
開場(chǎng)
Spring事務(wù)管理,在Service實(shí)現(xiàn)類的方法上添加@Transactional注解就能進(jìn)行事務(wù)控制,但是最近遇到一個(gè)事務(wù)失效的問題,淺淺分析一下
問題
在開發(fā)過程中發(fā)現(xiàn)這樣一個(gè)bug,有一段邏輯代碼:商品入庫、并且生成一個(gè)入庫單的操作;首先肯定的是需要操作兩張表,一是 入庫加庫存,二是 新增一個(gè)入庫單類似于入庫記錄。但是測(cè)試時(shí)候遇到一個(gè)問題,有一個(gè)商品入庫成功了,庫存也加進(jìn)去了,但是沒有生成入庫單,我立馬想到的是難道是事務(wù)沒有控制好?翻看接口代碼發(fā)現(xiàn)還真是。。。
Spring事務(wù)失效的原因
我們都知道@Transactional失效的原因有多種,列舉一下,后面有時(shí)間再仔細(xì)分析研究
1.方法訪問修飾符:
- @Transactional注解只對(duì)public方法生效。
- 如果事務(wù)性方法是private、protected,事務(wù)不會(huì)生效
2.類內(nèi)部方法調(diào)用:
- 在同一個(gè)類中調(diào)用標(biāo)注了**
@Transactional**的方法(即自調(diào)用)時(shí),事務(wù)管理器不會(huì)介入,因?yàn)镾pring AOP代理無法攔截內(nèi)部調(diào)用
3.沒有啟用事務(wù)管理:
- 必須顯式啟用Spring的事務(wù)管理,通常使用**@EnableTransactionManagement**注解在配置類上,或者在XML中配置使用
<tx:annotation-driven/>
4.事務(wù)傳播屬性不當(dāng):
- 事務(wù)傳播屬性(Propagetion)配置不當(dāng)會(huì)導(dǎo)致事務(wù)失效。
- 例如:傳播行為
REQUIRES_NEW會(huì)掛起當(dāng)前事務(wù),創(chuàng)建一個(gè)新事物,這可能不是我們想要的
5.異常處理不當(dāng):
- 默認(rèn)情況下,Spring事務(wù)管理只在運(yùn)行時(shí)異常(RuntimeExpection及其子類)和錯(cuò)誤(Error及其子類)時(shí)回滾。
- 如果捕獲并處理了一場(chǎng),但沒有重新拋出,事務(wù)不會(huì)回滾。此外需要注意,對(duì)于檢查異常(Exception及其子類),需要制定rollbackFor屬性來觸發(fā)事務(wù)回滾,例如:
@Transactional(rollbackFor = Exception.class)
6.多線程環(huán)境:
- Transactional 注解無法再多線程環(huán)境中傳播事務(wù)。
- Spring事務(wù)管理依賴于線程局部變量(ThreadLocal),在不同獻(xiàn)策會(huì)給你之間共享事務(wù)需要顯式處理。(后面遇到的話仔細(xì)研究一下)
7.數(shù)據(jù)源和事務(wù)管理器配置不一致:
- 確保數(shù)據(jù)源(DataSource)和事務(wù)管理器(TransactionManager)配置一致。
- 如果有多個(gè)數(shù)據(jù)源和事務(wù)管理器,需要明確指定使用的事務(wù)管理器。
8.Spring代理模式限制:
- Spring默認(rèn)使用AOP代理來管理事務(wù)。
- 默認(rèn)情況下使用JDK動(dòng)態(tài)代理,只有實(shí)現(xiàn)接口的類可以使用事務(wù)。
- 如果沒有實(shí)現(xiàn)接口,需要使用CGLIB代理,通過proxyTargetClass=true啟用CGLIB代理。
9.異步方法:
- 一步方法(如使用@Async注解的方法)運(yùn)行在獨(dú)立線程中,不會(huì)參與當(dāng)前線程的事務(wù)管理。
- 需要特別處理異步方法的事務(wù)管理。
排查bug
言歸正傳,我在這段邏輯代碼中發(fā)現(xiàn)有兩個(gè)問題,首先代碼塊被 try catch 捕獲后沒有重新拋出異常,而且這段代碼在catch 里面有日志記錄,插入到一張日志表;業(yè)務(wù)邏輯太多此處就不貼真實(shí)代碼了,但是我寫了一個(gè)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);
//日志記錄:將錯(cuò)誤信息插入到日志表
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)問題其他嗎,細(xì)心的小伙伴一定還能發(fā)現(xiàn)其他問題,那就是這個(gè) catch 捕獲異常后并沒有拋出,所以這段代碼并不會(huì)回滾,但是問題來了,如果我們?cè)?catch 中最后拋出異常,那么日志記錄的操作也將回滾,這里就會(huì)有點(diǎn)沖突,如果代碼有異常報(bào)錯(cuò),我們的目的就是要回滾,但是還不能把日志記錄給回滾。
解決方案
發(fā)現(xiàn)了問題,該如何去解決呢?
在這里我用到了一個(gè)事務(wù)的傳播行為:先說結(jié)論,將事務(wù)的傳播屬性設(shè)置為REQUIRES_NEW,就是在當(dāng)前事務(wù)中新建了一個(gè)事務(wù),用新的事務(wù)去控制我日志記錄的操作,這樣的話兩個(gè)事務(wù)互不影響,A事務(wù)如果拋出異常,不影響B(tài)事務(wù),B事務(wù)正常插入數(shù)據(jù),完美解決!當(dāng)然不止這一種解決方案,還有異步調(diào)用,mq發(fā)消息等方式。
但是需要注意,我們需要在單獨(dú)的Service實(shí)現(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)用時(shí)的傳播方式。傳播屬性通過 @Transactional 注解的 propagation 屬性來設(shè)置,Spring 提供了以下幾種傳播屬性:
1.REQUIRED(默認(rèn)值):
- 如果當(dāng)前已經(jīng)存在一個(gè)事務(wù),則加入該事務(wù)。
- 如果當(dāng)前沒有事務(wù),則創(chuàng)建一個(gè)新的事務(wù)。
- 這是最常見的傳播行為。
2.REQUIRES_NEW:
- 無論當(dāng)前是否存在事務(wù),總是創(chuàng)建一個(gè)新的事務(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:
- 必須在一個(gè)現(xiàn)有事務(wù)中運(yùn)行。
- 如果當(dāng)前沒有事務(wù),則拋出異常。
6.NEVER:
- 必須在非事務(wù)上下文中運(yùn)行。
- 如果當(dāng)前存在事務(wù),則拋出異常。
7.NESTED:
- 如果當(dāng)前存在事務(wù),則在嵌套事務(wù)中運(yùn)行。
- 如果當(dāng)前沒有事務(wù),則創(chuàng)建一個(gè)新的事務(wù)。
- 嵌套事務(wù)使用保存點(diǎn)(savepoint),如果嵌套事務(wù)回滾,它只回滾到保存點(diǎn),外部事務(wù)可以繼續(xù)。
- 如果當(dāng)前沒有事務(wù),則拋出異常。
8.NEVER:
- 必須在非事務(wù)上下文中運(yùn)行。
- 如果當(dāng)前存在事務(wù),則拋出異常。
9.NESTED:
- 如果當(dāng)前存在事務(wù),則在嵌套事務(wù)中運(yùn)行。
- 如果當(dāng)前沒有事務(wù),則創(chuàng)建一個(gè)新的事務(wù)。
- 嵌套事務(wù)使用保存點(diǎn)(savepoint),如果嵌套事務(wù)回滾,它只回滾到保存點(diǎn),外部事務(wù)可以繼續(xù)。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Idea如何使用Fast Request接口調(diào)試
這篇文章主要介紹了Idea如何使用Fast Request接口調(diào)試問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11
java中ImageReader和BufferedImage獲取圖片尺寸實(shí)例
這篇文章主要介紹了java中ImageReader和BufferedImage獲取圖片尺寸實(shí)例,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01
Java導(dǎo)出Excel動(dòng)態(tài)表頭的示例詳解
這篇文章主要為大家詳細(xì)介紹了Java導(dǎo)出Excel動(dòng)態(tài)表頭的相關(guān)知識(shí),文中的示例代碼簡(jiǎn)潔易懂,具有一定的借鑒價(jià)值,有需要的小伙伴可以了解下2025-02-02
面試官:詳細(xì)談?wù)凧ava對(duì)象的4種引用方式
這篇文章主要給大家介紹了java面試官常會(huì)問到的,關(guān)于Java對(duì)象的4種引用方式的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
詳解mysql插入數(shù)據(jù)后返回自增ID的七種方法
這篇文章主要介紹了詳解mysql插入數(shù)據(jù)后返回自增ID的七種方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
Intellij IDEA遠(yuǎn)程debug教程實(shí)戰(zhàn)和要點(diǎn)總結(jié)(推薦)
這篇文章主要介紹了Intellij IDEA遠(yuǎn)程debug教程實(shí)戰(zhàn)和要點(diǎn)總結(jié)(推薦),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
通過idea創(chuàng)建Spring Boot項(xiàng)目并配置啟動(dòng)過程圖解
這篇文章主要介紹了通過idea創(chuàng)建Spring Boot項(xiàng)目并配置啟動(dòng)過程圖解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11

