导致 Spring 事务失效的场景有哪些,如何解决失效问题?

实际项目开发中,如果涉及到多张表操作时,为了保证业务数据的一致性,大家一般都会采用事务机制;好多小伙伴可能只是简单了解一下,遇到事务失效的情况,便会无从下手,此篇文章给大家整理了一下常见Spring事务失效的场景,希望开发过程尽量避免踩坑,造成时间精力的浪费。

按照最基本的使用方式以及常见失效场景优先级整理,先简单介绍一下具体失效场景:

注解@Transactional配置的方法非public权限修饰;

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

注解@Transactional所在类中,注解修饰的方法被类内部方法调用;

业务代码抛出异常类型非RuntimeException,事务失效;

业务代码中存在异常时,使用try…catch…语句块捕获,而catch语句块没有throw new RuntimeExecption异常;(最难被排查到问题且容易忽略)

注解@Transactional中Propagation属性值设置错误即Propagation.NOT_SUPPORTED(一般不会设置此种传播机制)

mysql关系型数据库,且存储引擎是MyISAM而非InnoDB,则事务会不起作用(基本开发中不会遇到);

下面基于以上场景,给小伙伴们详细解释;

一、非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注释对受保护的、私有的或包可见的方法进行注释,则不会引发错误,但带注释的方法不会显示配置的事务设置。如果需要注释非公共方法,请考虑使用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();

}

}

三、注解修饰的方法被类内部方法调用

这种失效场景是我们日常开发中最常踩坑的地方;在类A里面有方法a 和方法b,然后方法b上面用@Transactional加了方法级别的事务,在方法a里面 调用了方法b, 方法b里面的事务不会生效。为什么会失效呢?:

其实原因很简单,Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于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()

虽然业务代码报错了,但是数据库中已经成功插入数据,事务并未生效;

解决方案

类内部使用其代理类调用事务方法:以上方法略作改动

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);

}

业务代码抛出异常,数据库未插入新数据,达到我们的目的,成功解决一个事务失效问题;

数据库数据未发生改变;

注意:一定要注意启动类上要添加@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{

//        即使此处使用代理对象调用内部事务方法,数据依然未发生回滚,事务机制亦然失效

((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);

}

}

运行结果:

业务代码抛出异常,但是数据库发生更新操作;

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);

}

执行结果:

解决方案:捕获异常并抛出异常

@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。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容