事务篇:Spring事务的坑,你都踩过吗?

Posted 青梅主码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了事务篇:Spring事务的坑,你都踩过吗?相关的知识,希望对你有一定的参考价值。


大家好,我是杰哥

关于事务,上一篇事务篇(一):毕业三年,你真的学会事务了吗?帮助大家回忆了它的一些基础概念。虽然基础,每个人都或多或少知道一点,但是基础往往决定你能走多远,能跳多高,这个也是工作时间越久。越能体会到的一点很重要的方法论

所以,建议大家千万不要忽略这些基础哦~

本篇,我们将要从本人以及同事在工作中踩过的关于事务的坑,以及踩坑之后自己在发现的使用 Spring 事务存在的坑展示给大家,让大家也避免踩坑

一 来看看这些事务之坑

总得来说呢,经常遇到的其实是这四类:自身调用异常被吃异常抛出类型不对以及事务的传播机制不熟悉

具体例子,我们来看看

1 数据库引擎不支持事务

感觉这种一般估计不太会出现。毕竟你要使用事务,肯定会在最开始就选择支持事务的数据库引擎咯

比如常用的 oracle 直接就是支持事务的,而 mysqlinnodb 支持事务,myIsam 的话,是不支持事务的

mysql5.1 版本之前,默认引擎是 myIsam ,而之后的版本则默认就是innodb 了~

建议检查项:mysql的数据库引擎

执行命令

show variables like '%storage_engine%';


image.png

我们看到,我本地的 5.7 版本 的 mysql 数据库的数据库引擎默认就是 innodb

2 方法不是 public 的

其实经过本人的测试,除了 private 方法本身就不能编译通过以外,public、protected 以及 default 三个修饰符都是支持事务的

事务篇(二):Spring事务的坑,你都踩过吗?
image.png

有兴趣,你也可以测试一下

3 自身调用问题

比如在同一个类中的两个方法 methodA 和 methodB 。methodA 没有设置事务,methodB 设置了事务,methodA 调用 methodB 时,事务便会失效

1) 同一个类中的方法调用

/**
 * 自调用测试:事务失效,表中新增了两条数据:id为10和11的数据
 */
@Override
public void testInvokeBInOneClass(){
    User user = User.builder().id(10).name("王二").age(22).build();
    userDao.addUser(user);
    testB();
}

@Transactional
public void testB(){
    User user = User.builder().id(11).name("张三").age(22).build();
    userDao.addUser(user);
    int i = 1/0;
}


我们预测一下

若事务失效,数据库中将会成功增加两条数据:王二和张三

若事务生效,则表中将不会增加任何数据

执行该方法后,我们会发现,数据库的 t_user2 表中的记录为

事务篇(二):Spring事务的坑,你都踩过吗?
image.png

没错,结果,事务失效了

2)不同类中的方法调用

我们把 testB () 方法放到另一个类 TransactionBImpl

事务篇(二):Spring事务的坑,你都踩过吗?
image.png

此时,调用 TransactionBImpl 类中的 testB () 方法

/**
 * 自调用测试:事务生效,表中新增了一条数据:id为10的数据
 */
@Override
public void testInvokeBInTwoClass(){
    User user = User.builder().id(10).name("王二").age(22).build();
    userDao.addUser(user);
    transactionB.testB();
}

发现数据库中的数据为:

事务篇(二):Spring事务的坑,你都踩过吗?
image.png

说明事务生效了,为什么呢?

因为外层 testInvokeBInTwoClass() 方法本身是没有事务(没有加事务注解)的,它调用了另一个类中 加了事务注解的 testB() 方法,不要忘记 @Transactional 注解的默认传播机制,是

PROPAGATION_REQUIRED - 若不存在事务,就要自己创建一个新事务

也就是说,最终的效果就是,testB() 方法内部在一个事务内,testInvokeBInTwoClass()方法中,并没有事务(不会因为异常而触发回滚操作)

那么,最终的结果,也就轻易理解咯~

如果你听过独立事务的话,就能想到它的实现机制了吧

Tips

有些业务需要,要求 methodA 调用 methodB 时,并不会因为 methodB 的执行失败,而影响了调用之前的操作。如在在表中调用之前登记了一条状态日志,此时并不想要因为调用失败,而回滚了这条记录,就可以这样操作啦~

小结

事务在发生自调用时,若调用方没有加 @Transactional 注解,事务便会失效


若要使事务生效,则可以考虑将该被调用的方法放在另一个类中即可

4 不支持事务

这种情况比较容易理解,只是会在编码过程中容易被忽略掉,所以在这里也提一下

methodA 调用另一个类中的 methodB ,若 methodB 设置了事务的传播机制为Propagation.NOT_SUPPORTED

那么,即使 methodA 开启了事务,也不一定会按照自己的预期来发展的,来看看下面这个例子:

UserServiceImpl 类

 @Override
public void testNotSupported() {
User user = User.builder().id(10).name("王二").age(22).build();
userDao.addUser(user);
transactionB.testNotSupported();
}

TransactionBImpl 类

@Transactional
(propagation = Propagation.NOT_SUPPORTED)
    @Override
    public void testNotSupported(){
        User user = User.builder().id(11).name("张三").age(22).build();
        userDao.addUser(user);
        int i = 1/0;
    }

即,UserServiceImpl 类中的 testNotSupported()方法调用了 TransactionBImpl 类 中的 testNotSupported()方法

我们来分析一下,按照调用方是否开启事务,可以分为以下两种情况 :

1)若调用方 testNotSupported()方法不加 @Transactional 注解,则表中数据为

事务篇(二):Spring事务的坑,你都踩过吗?
image.png

显而易见,说明两个方法统一都没有事务

若加上,则只插入了一条数据

事务篇(二):Spring事务的坑,你都踩过吗?
image.png

说明外部方法还是存在事务的,只要出现异常就会回滚。而被调用方 transactionB.testNotSupported() 的方法内部不支持事务,于是该方法出错之后也不会出现事务回滚,因此出错之前的插表操作就没有回滚

5 异常被catch住了,没有抛出来

由于事务默认回滚的是:RuntimeExceptionError 两种情况,所以以下两种情况都会失效

1)异常被吃了,事务失效

/**
  * 7、异常被吃了:try掉异常(未抛出),事务失效
 */
    @Transactional
    @Override
    public void testException(){
        try {
            User user = User.builder().id(10).name("王二").age(22).build();
            userDao.addUser(user);
            int i = 1/0;
        }catch (Exception e) {
            System.out.println("执行失败:"+e.getMessage());
//            throw new RuntimeException("执行失败,抛出异常:"+e.getMessage());
         }
    }


也就是说,异常并没有被抛出来,而是通过 catch 住,然后做了一些其他的逻辑处理,这种事务是不会生效的

再来看看第二种情况

2)抛出Exception异常,事务失效


@Transactional
    @Override
    public void testException() throws Exception {
        try {
            User user = User.builder().id(10).name("王二").age(22).build();
            userDao.addUser(user);
            int i = 1/0;
        }catch (Exception e) {
            System.out.println("执行失败:"+e.getMessage());
            throw new Exception("抛出了Exception异常:"+e.getMessage());
//            throw new RuntimeException("执行失败,抛出异常:"+e.getMessage());
        }
    }


回想一下我们的大前提:Spring事务默认回滚的是:RuntimeException和Error两种情况。现在抛出了 Excption ,就不会触发事务的回滚,所以这样事务也是不生效的

要怎样才能让这样的事务生效呢?

改成抛出 RuntimeException 事务就生效啦~ 你完全可以现在就试试

对了,如果你想触发其他异常的回滚,包括你自己定义的异常或者 Exception 异常的话,也不是没有办法。只需要在方法的注解上配置一下 rollbackFor 属性即可,如:

@Transactional(rollbackFor = Exception.class)

留一个思考题给你:若配置了其他异常,那原本的规则是否被覆盖掉?

小结

只要抓住一点:事务默认在:RuntimeException 和 Error 两种情况下执行回滚操作

因此,

1)异常被捕获掉,没有抛出来,就不会生效

2)抛出的 RuntimeException 异常或者未遇到 Error ,事务默认也不会生效的

那么,怎么处理才能让事务生效,想必已经很明显了吧?

6 未启用spring事务管理功能

@EnableTransactionManagement 注解用来启用spring事务自动管理事务的功能,只有有这个注解,这个注解千万不要忘记写了

但是当引入了

spring-boot-starter-jdbc

就可以不用我们自己写,为什么呢?我们来看看

事务篇(二):Spring事务的坑,你都踩过吗?
image.png

@EnableTransactionManagement 这个注解开启事务,其实和我们自己使用@EnableTransactionManagement是一样的 因此,只要我们在 SpringBoot 中引入了 spring-boot-starter-jdbc 这个依赖以后,我们就只需要使用 @Transactional 就可以了

二 总而言之

好了,本篇文章,接着上一篇的事务基础,为大家演示了几个开发过程中容易出现的事务失效,或者事务不能按照自己的预期来执行的几种场景

总结一下,日常中最容易出现事务失效或者不能按照预期执行的情况,大致分为四类:自身调用异常被吃异常抛出类型不对以及事务的传播机制不熟悉

那么我们需要如何去避免踩坑,正确高效地使用事务呢?

很简单,只需要关注单个方法时事务的回滚机制,以及涉及到两个以及两个以上方法的调用时事务的传播机制以及Spring事务的原理

1 单个方法的调用,事务只会在执行过程中出现 RuntimeException 和 Error 以及事务超时时进行事务的回滚;

2 多个方法:当在同一个类中进行方法调用时,若要事务不失效,则需要在调用方的方法都加上事务注解,同时需要关注事务的传播机制以及各层方法的事务回滚情况

不在同一类中时,则需要根据特定的业务场景,选择不同的传播机制

好了,那就到这边吧~

原创不易,如果本篇文章,对你有一点点帮助的话,记得点个在看哦~

嗯,就这样。每天学习一点,时间会见证你的强大~

下期预告:

事务篇(三):分享一个隐性事务失效场景


事务篇(二):Spring事务的坑,你都踩过吗?

往期精彩回顾


事务篇章




Cloud篇章


Spring Boot篇章
翻译



.........
职业、生活感悟


..........


欢迎大家关注们的公众号,一起持续性学习吧~


     



以上是关于事务篇:Spring事务的坑,你都踩过吗?的主要内容,如果未能解决你的问题,请参考以下文章

那些年,我们一起踩过的3个spring事务的大坑

[react] 在使用react过程中你都踩过哪些坑?你是怎么填坑的?

这几个SQL语法的坑,你踩过吗

这几个SQL语法的坑,你踩过吗

JQuery7个大坑,说说那些大家都踩过的坑

Spring常见的十大错误,78%的老程序员都踩过这些坑!