導致 Spring 事務失效的場景有哪些,如何解決失效問題?

實際項目開發(fā)中,如果涉及到多張表操作時,為了保證業(yè)務數據的一致性,大家一般都會采用事務機制;好多小伙伴可能只是簡單了解一下,遇到事務失效的情況,便會無從下手,此篇文章給大家整理了一下常見Spring事務失效的場景,希望開發(fā)過程盡量避免踩坑,造成時間精力的浪費。

按照最基本的使用方式以及常見失效場景優(yōu)先級整理,先簡單介紹一下具體失效場景:

注解@Transactional配置的方法非public權限修飾;

注解@Transactional所在類非Spring容器管理的bean;

注解@Transactional所在類中,注解修飾的方法被類內部方法調用;

業(yè)務代碼拋出異常類型非RuntimeException,事務失效;

業(yè)務代碼中存在異常時,使用try…catch…語句塊捕獲,而catch語句塊沒有throw new RuntimeExecption異常;(最難被排查到問題且容易忽略)

注解@Transactional中Propagation屬性值設置錯誤即Propagation.NOT_SUPPORTED(一般不會設置此種傳播機制)

mysql關系型數據庫,且存儲引擎是MyISAM而非InnoDB,則事務會不起作用(基本開發(fā)中不會遇到);

下面基于以上場景,給小伙伴們詳細解釋;

一、非public權限修飾

參考Spring官方文檔介紹,摘要、譯文如下:

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

譯文

使用代理時,您應該只將@Transactional注釋應用于具有公共可見性的方法。如果使用@Transactional注釋對受保護的、私有的或包可見的方法進行注釋,則不會引發(fā)錯誤,但帶注釋的方法不會顯示配置的事務設置。如果需要注釋非公共方法,請考慮使用AspectJ(見下文)。

簡言之:@Transactional 只能用于 public 的方法上,否則事務不會失效,如果要用在非 public 方法上,可以開啟 AspectJ 代理模式。

目前,如果@Transactional注解作用在非public方法上,編譯器也會給與明顯的提示,如圖:

二、非Spring容器管理的bean

基于這種失效場景,有工作經驗的大佬基本上是不會存在這種錯誤的;@Service注解注釋,StudentServiceImpl類則不會被Spring容器管理,因此即使方法被@Transactional注解修飾,事務也亦然不會生效。

簡單舉例如下:

/**

*@Author:qxy

*/

//@Service

publicclassStudentServiceImplimplementsStudentService{

@Autowired

privateStudentMapper?studentMapper;

@Autowired

privateClassService?classService;

@Override

@Transactional(propagation?=?Propagation.REQUIRED,?rollbackFor?=?Exception.class)

publicvoidinsertClassByException(StudentDostudentDo)throwsCustomException

{

studentMapper.insertStudent(studentDo);

thrownewCustomException();

}

}

三、注解修飾的方法被類內部方法調用

這種失效場景是我們日常開發(fā)中最常踩坑的地方;在類A里面有方法a 和方法b,然后方法b上面用@Transactional加了方法級別的事務,在方法a里面 調用了方法b, 方法b里面的事務不會生效。為什么會失效呢?:

其實原因很簡單,Spring在掃描Bean的時候會自動為標注了@Transactional注解的類生成一個代理類(proxy),當有注解的方法被調用的時候,實際上是代理類調用的,代理類在調用之前會開啟事務,執(zhí)行事務的操作,但是同類中的方法互相調用,相當于this.B(),此時的B方法并非是代理類調用,而是直接通過原有的Bean直接調用,所以注解會失效。

@Service

publicclassClassServiceImplimplementsClassService{

@Autowired

privateClassMapper?classMapper;

publicvoidinsertClass(ClassDo?classDo)throwsCustomException{

insertClassByException(classDo);

}

@Override

@Transactional(propagation?=?Propagation.REQUIRED)

publicvoidinsertClassByException(ClassDo?classDo)throwsCustomException{

classMapper.insertClass(classDo);

thrownewRuntimeException();

}

}

//測試用例:

@Test

publicvoidinsertInnerExceptionTest()throwsCustomException{

classDo.setClassId(2);

classDo.setClassName("java_2");

classDo.setClassNo("java_2");

classService.insertClass(classDo);

}

測試結果:

java.lang.RuntimeException

at?com.qxy.common.service.impl.ClassServiceImpl.insertClassByException(ClassServiceImpl.java:34)

at?com.qxy.common.service.impl.ClassServiceImpl.insertClass(ClassServiceImpl.java:27)

at?com.qxy.common.service.impl.ClassServiceImpl$$FastClassBySpringCGLIB$$a1c03d8.invoke()

雖然業(yè)務代碼報錯了,但是數據庫中已經成功插入數據,事務并未生效;

解決方案

類內部使用其代理類調用事務方法:以上方法略作改動

publicvoidinsertClass(ClassDo?classDo)throwsCustomException{

//?????????insertClassByException(classDo);

((ClassServiceImpl)AopContext.currentProxy()).insertClassByException(classDo);

}

//測試用例:

@Test

publicvoidinsertInnerExceptionTest()throwsCustomException{

classDo.setClassId(3);

classDo.setClassName("java_3");

classDo.setClassNo("java_3");

classService.insertClass(classDo);

}

業(yè)務代碼拋出異常,數據庫未插入新數據,達到我們的目的,成功解決一個事務失效問題;

數據庫數據未發(fā)生改變;

注意:一定要注意啟動類上要添加@EnableAspectJAutoProxy(exposeProxy = true)注解,否則啟動報錯:

java.lang.IllegalStateException:?Cannot?find?current?proxy:?Set'exposeProxy'property?on?Advised?to'true'to?make?it?available.

at?org.springframework.aop.framework.AopContext.currentProxy(AopContext.java:69)

at?com.qxy.common.service.impl.ClassServiceImpl.insertClass(ClassServiceImpl.java:28)

四、異常類型非RuntimeException

這種事務失效場景也是非常難排查問題的,如果沒有深究源碼實現,估計要花費一番功夫啦;

@Service

publicclassClassServiceImplimplementsClassService{

@Autowired

privateClassMapper?classMapper;

//????@Override

//????@Transactional(propagation?=?Propagation.NESTED,?rollbackFor?=?Exception.class)

publicvoidinsertClass(ClassDo?classDo)throwsException{

//????????即使此處使用代理對象調用內部事務方法,數據依然未發(fā)生回滾,事務機制亦然失效

((ClassServiceImpl)AopContext.currentProxy()).insertClassByException(classDo);

}

@Override

@Transactional(propagation?=?Propagation.REQUIRED)

publicvoidinsertClassByException(ClassDo?classDo)throwsException{

classMapper.insertClass(classDo);

//拋出非RuntimeException類型

thrownewException();

}

//測試用例:

@Test

publicvoidinsertInnerExceptionTest()throwsException{

classDo.setClassId(3);

classDo.setClassName("java_3");

classDo.setClassNo("java_3");

classService.insertClass(classDo);

}

}

運行結果:

業(yè)務代碼拋出異常,但是數據庫發(fā)生更新操作;

java.lang.Exception

at?com.qxy.common.service.impl.ClassServiceImpl.insertClassByException(ClassServiceImpl.java:35)

at?com.qxy.common.service.impl.ClassServiceImpl$$FastClassBySpringCGLIB$$a1c03d8.invoke()

at?org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)

數據庫依然插入數據,不是我們想要的結果啊,趕緊修改吧,產品經理來追啦~

解決方案:

@Transactional注解修飾的方法,加上rollbackfor屬性值,指定回滾異常類型:@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)

@Override

@Transactional(propagation?=?Propagation.REQUIRED,rollbackFor?=?Exception.class)

publicvoidinsertClassByException(ClassDoclassDo)throwsException

{

classMapper.insertClass(classDo);

thrownewException();

}

五、捕獲異常后,卻未拋出異常

在事務方法中使用try-catch,導致異常無法拋出,自然會導致事務失效。

@Service

publicclassClassServiceImplimplementsClassService{

@Autowired

privateClassMapper?classMapper;

//????@Override

publicvoidinsertClass(ClassDo?classDo){

((ClassServiceImpl)AopContext.currentProxy()).insertClassByException(classDo);

}

@Override

@Transactional(propagation?=?Propagation.REQUIRED,rollbackFor?=?Exception.class)

publicvoidinsertClassByException(ClassDoclassDo)

{

classMapper.insertClass(classDo);

try{

inti?=1/0;

}catch(Exception?e)?{

e.printStackTrace();

}

}

}

//?測試用例:

@Test

publicvoidinsertInnerExceptionTest(){

classDo.setClassId(4);

classDo.setClassName("java_4");

classDo.setClassNo("java_4");

classService.insertClass(classDo);

}

執(zhí)行結果:

解決方案:捕獲異常并拋出異常

@Override

@Transactional(propagation?=?Propagation.REQUIRED,rollbackFor?=?Exception.class)

publicvoidinsertClassByException(ClassDoclassDo)

{

classMapper.insertClass(classDo);

try{

inti?=1/0;

}catch(Exception?e)?{

e.printStackTrace();

thrownewRuntimeException();

}

}

六、事務傳播行為設置異常

此種事務傳播行為不是特殊自定義設置,基本上不會使用Propagation.NOT_SUPPORTED,不支持事務

@Transactional(propagation?=?Propagation.NOT_SUPPORTED,rollbackFor?=?Exception.class)

publicvoidinsertClassByException(ClassDoclassDo)

{

classMapper.insertClass(classDo);

try{

inti?=1/0;

}catch(Exception?e)?{

e.printStackTrace();

thrownewRuntimeException();

}

}

六、數據庫存儲引擎不支持事務

以MySQL關系型數據為例,如果其存儲引擎設置為 MyISAM,則事務失效,因為MyISMA 引擎是不支持事務操作的;

故若要事務生效,則需要設置存儲引擎為InnoDB ;目前 MySQL 從5.5.5版本開始默認存儲引擎是:InnoDB。

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容