spring聲明式事務(wù)@Transactional開發(fā)常犯的幾個錯誤及最新解決方案
目前JAVA的微服務(wù)項目基本都是SSM結(jié)構(gòu)(即:springCloud +springMVC+Mybatis),而其中Mybatis事務(wù)的管理也是交由spring來管理,大部份都是使用聲明式事務(wù)(@Transactional)來進行事務(wù)一致性的管理,然后在實際日常開發(fā)過程中,發(fā)現(xiàn)很多開發(fā)同學(xué)都用錯了spring聲明式事務(wù)(@Transactional)或者說使用非常不規(guī)范,導(dǎo)致出現(xiàn)各種事務(wù)問題。我(夢在旅途)今天周日休息,花了幾個小時把目前我已知的開發(fā)常犯的幾個錯誤都列舉出來并逐一分析根本原因同時針對原因給出解決方案及示例,希望能幫助到廣大JAVA開發(fā)者。
1. 事務(wù)不生效
問題現(xiàn)象:明明有事務(wù)注解,在事務(wù)方法內(nèi)部有拋錯,但事務(wù)卻沒有回滾,該執(zhí)行的SQL都執(zhí)行了。
示例代碼如下:(doInsert方法是有事務(wù)注解的)
/**
* @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;
}
}
//演示調(diào)用,最終打印出了ID為1的那條記錄,事務(wù)并沒有回滾
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");根本原因:沒有執(zhí)行事務(wù)AOP切面,因為在BEAN方法內(nèi)部直接調(diào)用另一個公開的事務(wù)方法,是原生的方法之間調(diào)用,并非是被代理后的BEAN方法,所以SPRING事務(wù)注解在這種情況下失去作用。
解決方案:不論是在BEAN外部或BEAN方法內(nèi)部,要確保一定是調(diào)用代理BEAN的公開事務(wù)方法,確保調(diào)用事務(wù)方法有被SPRING事務(wù)攔截處理,示例代碼如下:【在BEAN內(nèi)部則需要先注入BEAN本身的代理BEAN實例(有很多中獲取當(dāng)前BEAN的代理BEAN方案,在此不細(xì)說),然后通過代理BEAN調(diào)事務(wù)方法即可。】
/**
* @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調(diào)doInsert的事務(wù)方法,確保走切面
} 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;
}
}
//演示調(diào)用,最終打印出了none,說明事務(wù)有回滾,無法查出ID為1的那個記錄
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");2. 事務(wù)提交報錯
問題現(xiàn)象:事務(wù)方法內(nèi)有catch住錯誤,但卻無法正常提交事務(wù),報錯: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錯誤,但當(dāng)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;
}
}
//演示調(diào)用,最終有報錯:Transaction rolled back because it has been marked as rollback-only
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");根本原因:事務(wù)繼承“惹的禍”【事務(wù)傳播特性】,入口事務(wù)方法內(nèi)部再調(diào)其他事務(wù)方法,其他事務(wù)方法若有拋錯則會在方法返回時被事務(wù)切面標(biāo)記當(dāng)前事務(wù)僅能回滾,若最后入口事務(wù)方法執(zhí)行完成并想提交事務(wù)時卻因為事務(wù)是繼承的且有被標(biāo)記為僅能回滾后則只能報錯
解決方案:避免事務(wù)繼承 或 確保事務(wù)方法內(nèi)部不再調(diào)用其他事務(wù)方法(即:事務(wù)方法變成普通方法,小技巧參照我之前文章:任何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方法,此時是原生方法調(diào)用,不走事務(wù)切面,也就不會觸發(fā)事務(wù)記錄的情況
} 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,確保不繼承外部事務(wù)即可
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;
}
}
//演示調(diào)用,最終正確打印了ID為2的記錄,說明雖然插入ID=1的記錄失敗了,但插入2的記錄是正確的,入口事務(wù)有正確的提交
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");3. 事務(wù)不回滾
問題現(xiàn)象:事務(wù)方法內(nèi)部有報錯,但事務(wù)卻仍提交了,示例代碼如下:
/**代碼片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
//第一種情況:錯誤被catch住了
@Transactional
public DemoUser doGet1() {
try {
doInsert(1); //doInsert原生調(diào)用,代碼看似有事務(wù),實際此時無事務(wù),也就不存在事務(wù)回滾的情況
} catch (Exception ex) { //catch錯誤,doGet事務(wù)正常提交
System.out.println("insert error: " + ex.toString());
}
selfService.doInsert(2);
return demoUserMapper.get(2);
}
//第二種情況:外層報錯,內(nèi)層事務(wù)正常提交
@Transactional
public DemoUser doGet2() {
selfService.doInsert(2); //doInsert切面調(diào)用,有事務(wù)且單獨事務(wù),執(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;
}
//演示調(diào)用,第1種情況
DemoUser result = demoUserService.doGet1();
System.out.println(result != null ? result.toString() : "none");
//演示調(diào)用,第2種情況
DemoUser result = demoUserService.doGet2();
System.out.println(result != null ? result.toString() : "none");根本原因:一是錯誤被catch住了,這種情況下事務(wù)切面認(rèn)為是正常的則會正常執(zhí)行提交事務(wù),二是根本就沒有事務(wù)或事務(wù)并非同一個事務(wù)(與事務(wù)傳播特性有關(guān)),這種情況就好理解,沒事務(wù)就不存在事務(wù)提交(方法中的每個SQL即為一個小事務(wù),執(zhí)行即提交),若是事務(wù)方法內(nèi)部有嵌套調(diào)用其他事務(wù)方法,入口的外層事務(wù)會受內(nèi)部其他事務(wù)方法的影響,反之若其他事務(wù)方法與外層事務(wù)不是同一個事務(wù),那么外層事務(wù)有報錯并不會影響內(nèi)部其他事務(wù)方法
- 這里還補充一種特殊情況,若在事務(wù)方法中異步調(diào)用其他事務(wù)方法(@Async 或線程池直接調(diào)用等情況),那么由于不在同一個線程上下文,即使默認(rèn)是繼承的傳播特性也無變成2個不相干的事務(wù)各自執(zhí)行,異步事務(wù)方法的報錯不會影響外層的事務(wù)方法
解決方案:若需保證事務(wù)的完整性,需確保若有異常一定要拋錯而非catch錯誤,另外需確保一定有事務(wù),當(dāng)事務(wù)方法內(nèi)部有嵌套調(diào)用其他事務(wù)方法時,若希望被調(diào)用的事務(wù)方法與當(dāng)前事務(wù)保持一致,那么就應(yīng)確保是事務(wù)繼承,否則就說明可以允許局部事務(wù)不一致,示例代碼如下:
/**代碼片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Transactional
public DemoUser doGet() {
doInsert(1);//不要catch,若catch后記錄日志后再拋出,總之一定要拋錯
selfService.doInsert(1);//這種也可以,當(dāng)doInsert報錯,則doInsert與doGet方法均回滾(本質(zhì)是同一個事務(wù))
selfService.doInsert(2);
return demoUserMapper.get(2);
}
@Transactional(propagation = Propagation.REQUIRED) //若需與外層事務(wù)這一致,這里建議采用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); //這里是原生方法調(diào)用,等同于在doGetX同一個事務(wù)方法內(nèi)部執(zhí)行
user.setName("xxx2");
selfService.update(user); //這里新開事務(wù)調(diào)用,由于doGetX中已經(jīng)有調(diào)用update(id=1)且事務(wù)還未提交,故這里需要等待doGetX事務(wù)提交以便釋放鎖,而doGetX事務(wù)則因為這里等待無法往下執(zhí)行,形成事務(wù)循環(huán)自依賴了
return demoUserMapper.get(1);
}
@Transactional(propagation = Propagation.REQUIRES_NEW) //這里新開事務(wù)
public int update(DemoUser demoUser) {
return demoUserMapper.update(demoUser);
}
//演示調(diào)用,執(zhí)行報錯,不同DB的報錯提示可能有所不同
DemoUser result = demoUserService.doGetX();
System.out.println(result != null ? result.toString() : "none"); 根本原因:事務(wù)被循環(huán)自依賴了,再準(zhǔn)確的說就是同一個記錄被2個事務(wù)相互依賴,導(dǎo)致相互等待獲取鎖
解決方案:避免事務(wù)被循環(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); //這里是原生方法調(diào)用,等同于在doGetX同一個事務(wù)方法內(nèi)部執(zhí)行
user.setName("xxx2");
update(user); //這里也改為原生方法調(diào)用,等同于在doGetX同一個事務(wù)方法內(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方法調(diào)用,新開事務(wù),直接執(zhí)行并提交,與doGetX事務(wù)互不影響
user.setName("xxx2");
selfService.update(user); //這里是代理BEAN方法調(diào)用,新開事務(wù),直接執(zhí)行并提交,與doGetX事務(wù)互不影響
return demoUserMapper.get(1);
}
@Transactional(propagation = Propagation.REQUIRES_NEW) //這里新開事務(wù)
public int update(DemoUser demoUser) {
return demoUserMapper.update(demoUser);
}
//演示調(diào)用,執(zhí)行報錯,不同DB的報錯提示可能有所不同
DemoUser result = demoUserService.doGetX();
System.out.println(result != null ? result.toString() : "none"); 5. 在事務(wù)提交后回調(diào)事件方法中開事務(wù)不生效
問題現(xiàn)象:在事務(wù)提交后回調(diào)事件方法中【即:afterCommit】開啟事務(wù)不生效(即:添加了@Transactional,也執(zhí)行了代理方法的調(diào)用,但就像沒有事務(wù)一樣,出現(xiàn)報錯事務(wù)不回滾,也無法在事務(wù)方法中再次注冊事務(wù)提交后回調(diào)事務(wù)件方法),示例代碼如下:
/**代碼片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Transactional
public DemoUser doGetX() {
doInsert(1);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
selfService.doInsert(2);//走切面調(diào)用,確保執(zhí)行代理的事務(wù)方法,但實際還是無事務(wù),報錯也不會回滾
}
});
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;
}
//演示調(diào)用:雖然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");根本原因:在事務(wù)提交后回調(diào)事件方法中【即:afterCommit】,spring事務(wù)的管理狀態(tài)仍保留(即:仍是事務(wù)激活狀態(tài))但DB事務(wù)其實已提交,當(dāng)回調(diào)方法中又遇到有事務(wù)注解的方法時且判斷已有事務(wù)(即spring事務(wù)的管理狀態(tài)是激活狀態(tài)transactionActive=true)時,若是默認(rèn)繼承狀態(tài)則不會再開啟新事務(wù),僅復(fù)用DB連接
解決方案:在事務(wù)提交后回調(diào)事件方法中【即:afterCommit】開啟新事務(wù)(即:傳播特性為:REQUIRES_NEW) 或者 執(zhí)行前強制清除事務(wù)狀態(tài)【需要編寫事務(wù)狀態(tài)清除工具類】,示例代碼如下:
/**代碼片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Transactional
public DemoUser doGetX() {
TxManagerUtils.clearTxStatus();//方案二:通過事務(wù)狀態(tài)清除工具類注冊事務(wù)回調(diào)后首先清除事務(wù)狀態(tài),二選其一即可
doInsert(1);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
selfService.doInsert(2);//走切面調(diào)用,確保執(zhí)行代理的事務(wù)方法
}
});
return demoUserMapper.get(1);
}
@Transactional(propagation = Propagation.REQUIRES_NEW) //方案一:這里強制開啟新事務(wù),二選其一即可
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;
}
//演示調(diào)用:雖然doGetX有報錯,但只能查出ID=1的記錄,ID=2由于報錯事務(wù)回滾了,說明afterCommit中再開啟事務(wù)是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");事務(wù)狀態(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 {
//建議在每個事務(wù)方法的第一行調(diào)用,避免事務(wù)方法內(nèi)部中途若有其他方法需要注冊事務(wù)提交后回調(diào)方法
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(); //第一個回調(diào)事件中先清除事務(wù)狀態(tài)
}
@Override
public void afterCompletion(int status) {
TransactionSynchronizationManager.bindResource(dataSource, connectionHolder); //恢復(fù)DB連接綁定,避免執(zhí)行事務(wù)清理時報錯
}
});
}
private static void doClearTxStatus() {
DataSource dataSource = SpringUtils.getBean(DataSource.class);
TransactionSynchronizationManager.setActualTransactionActive(false); //設(shè)置事務(wù)狀態(tài)為非激活
ConnectionHolder connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
connectionHolder.setTransactionActive(false);//設(shè)置事務(wù)狀態(tài)為非激活
TransactionSynchronizationManager.unbindResource(dataSource); //暫時解綁DB連接
}
}注:后面我預(yù)計還會針對spring事務(wù)這塊進行其他方面的分享(比如:spring事務(wù)在多數(shù)據(jù)源中切換數(shù)據(jù)源不生效、事務(wù)隔離級別下的并發(fā)處理等),敬請期待,原創(chuàng)不易,若有不足歡迎指出,謝謝!
到此這篇關(guān)于spring聲明式事務(wù)@Transactional開發(fā)常犯的幾個錯誤及解決辦法的文章就介紹到這了,更多相關(guān)spring聲明式事務(wù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot使用Jasypt對配置文件和數(shù)據(jù)庫密碼加密
在做數(shù)據(jù)庫敏感信息保護時,應(yīng)加密存儲,本文就來介紹一下SpringBoot使用Jasypt對配置文件和數(shù)據(jù)庫密碼加密,具有一定的參考價值,感興趣的可以了解一下2024-02-02
Java Swing實現(xiàn)窗體添加背景圖片的2種方法詳解
這篇文章主要介紹了Java Swing實現(xiàn)窗體添加背景圖片的2種方法,結(jié)合實例形式較為詳細(xì)的分析了Swing實現(xiàn)窗體添加背景圖片的方法,并總結(jié)分析了Swing重繪中repaint與updateUI的區(qū)別,需要的朋友可以參考下2017-11-11
Java實現(xiàn)獲取銀行卡所屬銀行,驗證銀行卡號是否正確的方法詳解
這篇文章主要介紹了Java實現(xiàn)獲取銀行卡所屬銀行,驗證銀行卡號是否正確的方法,結(jié)合實例形式詳細(xì)分析了java判斷銀行卡歸屬地及有效性的原理與相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2019-09-09
Java Swing SpringLayout彈性布局的實現(xiàn)代碼
這篇文章主要介紹了Java Swing SpringLayout彈性布局的實現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
Spring MVC項目中l(wèi)og4J和AOP使用詳解
項目日志記錄是項目開發(fā)、運營必不可少的內(nèi)容,有了它可以對系統(tǒng)有整體的把控,出現(xiàn)任何問題都有蹤跡可尋。下面這篇文章主要給大家介紹了關(guān)于Spring MVC項目中l(wèi)og4J和AOP使用的相關(guān)資料,需要的朋友可以參考下。2017-12-12
常用數(shù)字簽名算法RSA與DSA的Java程序內(nèi)實現(xiàn)示例
這篇文章主要介紹了常用數(shù)字簽名算法RSA與DSA的Java程序內(nèi)實現(xiàn)示例,一般來說DSA算法用于簽名的效率會比RSA要快,需要的朋友可以參考下2016-04-04

