spring聲明式事務@Transactional開發(fā)常犯的幾個錯誤及最新解決方案
目前JAVA的微服務項目基本都是SSM結構(即:springCloud +springMVC+Mybatis),而其中Mybatis事務的管理也是交由spring來管理,大部份都是使用聲明式事務(@Transactional)來進行事務一致性的管理,然后在實際日常開發(fā)過程中,發(fā)現(xiàn)很多開發(fā)同學都用錯了spring聲明式事務(@Transactional)或者說使用非常不規(guī)范,導致出現(xiàn)各種事務問題。我(夢在旅途)今天周日休息,花了幾個小時把目前我已知的開發(fā)常犯的幾個錯誤都列舉出來并逐一分析根本原因同時針對原因給出解決方案及示例,希望能幫助到廣大JAVA開發(fā)者。
1. 事務不生效
問題現(xiàn)象:明明有事務注解,在事務方法內(nèi)部有拋錯,但事務卻沒有回滾,該執(zhí)行的SQL都執(zhí)行了。
示例代碼如下:(doInsert方法是有事務注解的)
/**
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Service
public class DemoUserService {
//... ...
public DemoUser doGet() {
try {
doInsert(1);
} catch (Exception ex) {
System.out.println("insert error: " + ex.toString());
}
return demoUserMapper.get(1);
}
@Transactional
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
int result = demoUserMapper.insert(user);
throw new RuntimeException("mock insert ex"); //模擬拋錯
return result;
}
}
//演示調用,最終打印出了ID為1的那條記錄,事務并沒有回滾
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");根本原因:沒有執(zhí)行事務AOP切面,因為在BEAN方法內(nèi)部直接調用另一個公開的事務方法,是原生的方法之間調用,并非是被代理后的BEAN方法,所以SPRING事務注解在這種情況下失去作用。
解決方案:不論是在BEAN外部或BEAN方法內(nèi)部,要確保一定是調用代理BEAN的公開事務方法,確保調用事務方法有被SPRING事務攔截處理,示例代碼如下:【在BEAN內(nèi)部則需要先注入BEAN本身的代理BEAN實例(有很多中獲取當前BEAN的代理BEAN方案,在此不細說),然后通過代理BEAN調事務方法即可?!?/p>
/**
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Service
public class DemoUserService {
@Autowired
@Lazy //加上這個,是防止循環(huán)自依賴
private DemoUserService selfService; //注入自己的代理BEAN實例
//... ...
public DemoUser doGet() {
try {
selfService.doInsert(1); //這里改為使用代理BEAN調doInsert的事務方法,確保走切面
} catch (Exception ex) {
System.out.println("insert error: " + ex.toString());
}
return demoUserMapper.get(1);
}
@Transactional
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
int result = demoUserMapper.insert(user);
throw new RuntimeException("mock insert ex");
// return result;
}
}
//演示調用,最終打印出了none,說明事務有回滾,無法查出ID為1的那個記錄
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");2. 事務提交報錯
問題現(xiàn)象:事務方法內(nèi)有catch住錯誤,但卻無法正常提交事務,報錯:Transaction rolled back because it has been marked as rollback-only,示例代碼如下:
/**
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Service
public class DemoUserService {
@Autowired
@Lazy //加上這個,是防止循環(huán)自依賴
private DemoUserService selfService; //注入自己的代理BEAN實例
//... ...
@Transactional
public DemoUser doGet() {
try {
selfService.doInsert(1);
} catch (Exception ex) { //有catch錯誤,但當doGet返回時卻報錯了
System.out.println("insert error: " + ex.toString());
}
return demoUserMapper.get(1);
}
@Transactional
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
int result = demoUserMapper.insert(user);
if (id==1) {
throw new RuntimeException("mock insert ex");
}
return result;
}
}
//演示調用,最終有報錯:Transaction rolled back because it has been marked as rollback-only
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");根本原因:事務繼承“惹的禍”【事務傳播特性】,入口事務方法內(nèi)部再調其他事務方法,其他事務方法若有拋錯則會在方法返回時被事務切面標記當前事務僅能回滾,若最后入口事務方法執(zhí)行完成并想提交事務時卻因為事務是繼承的且有被標記為僅能回滾后則只能報錯
解決方案:避免事務繼承 或 確保事務方法內(nèi)部不再調用其他事務方法(即:事務方法變成普通方法,小技巧參照我之前文章:任何Bean通過實現(xiàn)ProxyableBeanAccessor接口即可獲得動態(tài)靈活的獲取代理對象或原生對象的能力 - 夢在旅途 - 博客園 (cnblogs.com)),示例代碼如下:
/**
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Service
public class DemoUserService {
@Autowired
@Lazy //加上這個,是防止循環(huán)自依賴
private DemoUserService selfService; //注入自己的代理BEAN實例
//... ...
@Transactional
public DemoUser doGet() {
try {
selfService.doInsert(1);
// doInsert(1); 方案二:內(nèi)部直接doInsert方法,此時是原生方法調用,不走事務切面,也就不會觸發(fā)事務記錄的情況
} catch (Exception ex) { //有catch錯誤
System.out.println("insert error: " + ex.toString());
}
selfService.doInsert(2);
return demoUserMapper.get(2);
}
@Transactional(propagation = Propagation.REQUIRES_NEW) //方案一:這里加上REQUIRES_NEW、或NOT_SUPPORTED,確保不繼承外部事務即可
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
int result = demoUserMapper.insert(user);
if (id==1) {
throw new RuntimeException("mock insert ex");
}
return result;
}
}
//演示調用,最終正確打印了ID為2的記錄,說明雖然插入ID=1的記錄失敗了,但插入2的記錄是正確的,入口事務有正確的提交
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");3. 事務不回滾
問題現(xiàn)象:事務方法內(nèi)部有報錯,但事務卻仍提交了,示例代碼如下:
/**代碼片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
//第一種情況:錯誤被catch住了
@Transactional
public DemoUser doGet1() {
try {
doInsert(1); //doInsert原生調用,代碼看似有事務,實際此時無事務,也就不存在事務回滾的情況
} catch (Exception ex) { //catch錯誤,doGet事務正常提交
System.out.println("insert error: " + ex.toString());
}
selfService.doInsert(2);
return demoUserMapper.get(2);
}
//第二種情況:外層報錯,內(nèi)層事務正常提交
@Transactional
public DemoUser doGet2() {
selfService.doInsert(2); //doInsert切面調用,有事務且單獨事務,執(zhí)行完即提交
throw new RuntimeException("mock doGet ex");//這里拋錯不影響doInsert的提交
return demoUserMapper.get(2);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
int result = demoUserMapper.insert(user);
if (id==1) {
throw new RuntimeException("mock insert ex");
}
return result;
}
//演示調用,第1種情況
DemoUser result = demoUserService.doGet1();
System.out.println(result != null ? result.toString() : "none");
//演示調用,第2種情況
DemoUser result = demoUserService.doGet2();
System.out.println(result != null ? result.toString() : "none");根本原因:一是錯誤被catch住了,這種情況下事務切面認為是正常的則會正常執(zhí)行提交事務,二是根本就沒有事務或事務并非同一個事務(與事務傳播特性有關),這種情況就好理解,沒事務就不存在事務提交(方法中的每個SQL即為一個小事務,執(zhí)行即提交),若是事務方法內(nèi)部有嵌套調用其他事務方法,入口的外層事務會受內(nèi)部其他事務方法的影響,反之若其他事務方法與外層事務不是同一個事務,那么外層事務有報錯并不會影響內(nèi)部其他事務方法
- 這里還補充一種特殊情況,若在事務方法中異步調用其他事務方法(@Async 或線程池直接調用等情況),那么由于不在同一個線程上下文,即使默認是繼承的傳播特性也無變成2個不相干的事務各自執(zhí)行,異步事務方法的報錯不會影響外層的事務方法
解決方案:若需保證事務的完整性,需確保若有異常一定要拋錯而非catch錯誤,另外需確保一定有事務,當事務方法內(nèi)部有嵌套調用其他事務方法時,若希望被調用的事務方法與當前事務保持一致,那么就應確保是事務繼承,否則就說明可以允許局部事務不一致,示例代碼如下:
/**代碼片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Transactional
public DemoUser doGet() {
doInsert(1);//不要catch,若catch后記錄日志后再拋出,總之一定要拋錯
selfService.doInsert(1);//這種也可以,當doInsert報錯,則doInsert與doGet方法均回滾(本質是同一個事務)
selfService.doInsert(2);
return demoUserMapper.get(2);
}
@Transactional(propagation = Propagation.REQUIRED) //若需與外層事務這一致,這里建議采用REQUIRED的傳播特性
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
int result = demoUserMapper.insert(user);
if (id==1) {
throw new RuntimeException("mock insert ex");
}
return result;
}4. 死鎖
問題現(xiàn)象:執(zhí)行SQL有報死鎖,示例代碼如下:
/**代碼片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Transactional
public DemoUser doGetX() {
selfService.doInsert(1);
DemoUser user=selfService.get(1);
user.setName("xxx");
update(user); //這里是原生方法調用,等同于在doGetX同一個事務方法內(nèi)部執(zhí)行
user.setName("xxx2");
selfService.update(user); //這里新開事務調用,由于doGetX中已經(jīng)有調用update(id=1)且事務還未提交,故這里需要等待doGetX事務提交以便釋放鎖,而doGetX事務則因為這里等待無法往下執(zhí)行,形成事務循環(huán)自依賴了
return demoUserMapper.get(1);
}
@Transactional(propagation = Propagation.REQUIRES_NEW) //這里新開事務
public int update(DemoUser demoUser) {
return demoUserMapper.update(demoUser);
}
//演示調用,執(zhí)行報錯,不同DB的報錯提示可能有所不同
DemoUser result = demoUserService.doGetX();
System.out.println(result != null ? result.toString() : "none"); 根本原因:事務被循環(huán)自依賴了,再準確的說就是同一個記錄被2個事務相互依賴,導致相互等待獲取鎖
解決方案:避免事務被循環(huán)自依賴,示列代碼如下:
/**代碼片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
//優(yōu)化一
@Transactional
public DemoUser doGetX() {
selfService.doInsert(1);
DemoUser user=selfService.get(1);
user.setName("xxx");
update(user); //這里是原生方法調用,等同于在doGetX同一個事務方法內(nèi)部執(zhí)行
user.setName("xxx2");
update(user); //這里也改為原生方法調用,等同于在doGetX同一個事務方法內(nèi)部執(zhí)行
return demoUserMapper.get(1);
}
//優(yōu)化二
@Transactional
public DemoUser doGetX() {
selfService.doInsert(1);
DemoUser user=selfService.get(1);
user.setName("xxx");
selfService.update(user); //這里是代理BEAN方法調用,新開事務,直接執(zhí)行并提交,與doGetX事務互不影響
user.setName("xxx2");
selfService.update(user); //這里是代理BEAN方法調用,新開事務,直接執(zhí)行并提交,與doGetX事務互不影響
return demoUserMapper.get(1);
}
@Transactional(propagation = Propagation.REQUIRES_NEW) //這里新開事務
public int update(DemoUser demoUser) {
return demoUserMapper.update(demoUser);
}
//演示調用,執(zhí)行報錯,不同DB的報錯提示可能有所不同
DemoUser result = demoUserService.doGetX();
System.out.println(result != null ? result.toString() : "none"); 5. 在事務提交后回調事件方法中開事務不生效
問題現(xiàn)象:在事務提交后回調事件方法中【即:afterCommit】開啟事務不生效(即:添加了@Transactional,也執(zhí)行了代理方法的調用,但就像沒有事務一樣,出現(xiàn)報錯事務不回滾,也無法在事務方法中再次注冊事務提交后回調事務件方法),示例代碼如下:
/**代碼片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Transactional
public DemoUser doGetX() {
doInsert(1);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
selfService.doInsert(2);//走切面調用,確保執(zhí)行代理的事務方法,但實際還是無事務,報錯也不會回滾
}
});
return demoUserMapper.get(1);
}
@Transactional(propagation = Propagation.REQUIRED)
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
int result = demoUserMapper.insert(user);
if (id==2) {
throw new RuntimeException("mock insert ex");
}
return result;
}
//演示調用:雖然doGetX有報錯,但最終doInsert方法均有執(zhí)行,且都能查出ID=1 與2的記錄
try {
DemoUser result = demoUserService.doGetX();
System.out.println(result != null ? result.toString() : "none");
}catch (Exception e){
System.out.println("error " + e.toString());
}
DemoUser result1 =demoUserService.get(1);
System.out.println(result1 != null ? result1.toString() : "none");
DemoUser result2 =demoUserService.get(2);
System.out.println(result2 != null ? result2.toString() : "none");根本原因:在事務提交后回調事件方法中【即:afterCommit】,spring事務的管理狀態(tài)仍保留(即:仍是事務激活狀態(tài))但DB事務其實已提交,當回調方法中又遇到有事務注解的方法時且判斷已有事務(即spring事務的管理狀態(tài)是激活狀態(tài)transactionActive=true)時,若是默認繼承狀態(tài)則不會再開啟新事務,僅復用DB連接
解決方案:在事務提交后回調事件方法中【即:afterCommit】開啟新事務(即:傳播特性為:REQUIRES_NEW) 或者 執(zhí)行前強制清除事務狀態(tài)【需要編寫事務狀態(tài)清除工具類】,示例代碼如下:
/**代碼片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Transactional
public DemoUser doGetX() {
TxManagerUtils.clearTxStatus();//方案二:通過事務狀態(tài)清除工具類注冊事務回調后首先清除事務狀態(tài),二選其一即可
doInsert(1);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
selfService.doInsert(2);//走切面調用,確保執(zhí)行代理的事務方法
}
});
return demoUserMapper.get(1);
}
@Transactional(propagation = Propagation.REQUIRES_NEW) //方案一:這里強制開啟新事務,二選其一即可
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis()));
int result = demoUserMapper.insert(user);
if (id==2) {
throw new RuntimeException("mock insert ex");
}
return result;
}
//演示調用:雖然doGetX有報錯,但只能查出ID=1的記錄,ID=2由于報錯事務回滾了,說明afterCommit中再開啟事務是OK的
try {
DemoUser result = demoUserService.doGetX();
System.out.println(result != null ? result.toString() : "none");
}catch (Exception e){
System.out.println("error " + e.toString());
}
DemoUser result1 =demoUserService.get(1);
System.out.println(result1 != null ? result1.toString() : "none");
DemoUser result2 =demoUserService.get(2);
System.out.println(result2 != null ? result2.toString() : "none");事務狀態(tài)清除工具類如下:
package org.springframework.jdbc.datasource; //必需放在這個包目錄下,因為connectionHolder.setTransactionActive 是protected方法
import com.example.springwebapp.utils.SpringUtils;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.sql.DataSource;
/**
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
public class TxManagerUtils {
//建議在每個事務方法的第一行調用,避免事務方法內(nèi)部中途若有其他方法需要注冊事務提交后回調方法
public static void clearTxStatus() {
DataSource dataSource = SpringUtils.getBean(DataSource.class);
ConnectionHolder connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public int getOrder() {
return Integer.MIN_VALUE; //確保最先執(zhí)行
}
@Override
public void afterCommit() {
doClearTxStatus(); //第一個回調事件中先清除事務狀態(tài)
}
@Override
public void afterCompletion(int status) {
TransactionSynchronizationManager.bindResource(dataSource, connectionHolder); //恢復DB連接綁定,避免執(zhí)行事務清理時報錯
}
});
}
private static void doClearTxStatus() {
DataSource dataSource = SpringUtils.getBean(DataSource.class);
TransactionSynchronizationManager.setActualTransactionActive(false); //設置事務狀態(tài)為非激活
ConnectionHolder connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
connectionHolder.setTransactionActive(false);//設置事務狀態(tài)為非激活
TransactionSynchronizationManager.unbindResource(dataSource); //暫時解綁DB連接
}
}注:后面我預計還會針對spring事務這塊進行其他方面的分享(比如:spring事務在多數(shù)據(jù)源中切換數(shù)據(jù)源不生效、事務隔離級別下的并發(fā)處理等),敬請期待,原創(chuàng)不易,若有不足歡迎指出,謝謝!
到此這篇關于spring聲明式事務@Transactional開發(fā)常犯的幾個錯誤及解決辦法的文章就介紹到這了,更多相關spring聲明式事務內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
詳談java中int和Integer的區(qū)別及自動裝箱和自動拆箱
這篇文章主要介紹了詳談java中int和Integer的區(qū)別及自動裝箱和自動拆箱,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08
Java實現(xiàn)經(jīng)典游戲推箱子的示例代碼
《推箱子》推箱子是一個古老的游戲,目的是在訓練你的邏輯思考能力。本文將利用Java實現(xiàn)這一經(jīng)典的小游戲,并采用了swing技術進行了界面化處理,需要的可以參考一下2022-02-02
Spring?Boot網(wǎng)絡配置之server.address超詳細講解
在Spring Boot中,我們可以通過配置server.address屬性來指定應用程序綁定的IP地址,下面這篇文章主要介紹了Spring?Boot網(wǎng)絡配置之server.address的相關資料,需要的朋友可以參考下2025-09-09
使用java對接WebService接口實現(xiàn)過程
文章介紹了WebService的概念、應用場景和對接方式,特別強調了Java中對接WebService的方法,包括JAX-WS、第三方庫和Spring的WebServiceTemplate,還分享了使用Hutool工具的方法,并通過實例展示了服務端和客戶端的實現(xiàn),以及使用SoapUI進行驗證和測試的過程2026-04-04

