聊聊spring事务失效的12种场景,太坑了

Posted 小哈学Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊spring事务失效的12种场景,太坑了相关的知识,希望对你有一定的参考价值。

如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
  • SUPPORTS 如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
  • MANDATORY 如果当前上下文中存在事务,否则抛出异常。
  • REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
  • NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
  • NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
  • NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
  • 如果我们在手动设置propagation参数的时候,把传播特性设置错了,比如:

    @Service
    public class UserService 

        @Transactional(propagation = Propagation.NEVER)
        public void add(UserModel userModel) 
            saveData(userModel);
            updateData(userModel);
        

    我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。

    目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。

    2.自己吞了异常

    事务不会回滚,最常见的问题是:开发者在代码中手动try...catch了异常。比如:

    @Slf4j
    @Service
    public class UserService 
        
        @Transactional
        public void add(UserModel userModel) 
            try 
                saveData(userModel);
                updateData(userModel);
             catch (Exception e) 
                log.error(e.getMessage(), e);
            
        

    这种情况下spring事务当然不会回滚,因为开发者自己捕获了异常,又没有手动抛出,换句话说就是把异常吞掉了。

    如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。

    3.手动抛了别的异常

    即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。

    @Slf4j
    @Service
    public class UserService 
        
        @Transactional
        public void add(UserModel userModel) throws Exception 
            try 
                 saveData(userModel);
                 updateData(userModel);
             catch (Exception e) 
                log.error(e.getMessage(), e);
                throw new Exception(e);
            
        

    上面的这种情况,开发人员自己捕获了异常,又手动抛出了异常:Exception,事务同样不会回滚。

    因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。

    4.自定义了回滚异常

    在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。

    但如果这个参数的值设置错了,就会引出一些莫名其妙的问题,例如:

    @Slf4j
    @Service
    public class UserService 
        
        @Transactional(rollbackFor = BusinessException.class)
        public void add(UserModel userModelthrows Exception 

           saveData(userModel);
           updateData(userModel);
        

    如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。

    即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。

    这是为什么呢?

    因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一般情况下,将该参数设置成:Exception或Throwable。

    5.嵌套事务回滚多了
    public class UserService 

        @Autowired
        private UserMapper userMapper;

        @Autowired
        private RoleService roleService;

        @Transactional
        public void add(UserModel userModel) throws Exception 
            userMapper.insertUser(userModel);
            roleService.doOtherThing();
        


    @Service
    public class RoleService 

        @Transactional(propagation = Propagation.NESTED)
        public void doOtherThing() 
            System.out.println("保存role表数据");
        

    这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。。但事实是,insertUser也回滚了。

    why?

    因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

    怎么样才能只回滚保存点呢?

    @Slf4j
    @Service
    public class UserService 

        @Autowired
        private UserMapper userMapper;

        @Autowired
        private RoleService roleService;

        @Transactional
        public void add(UserModel userModel) throws Exception 

            userMapper.insertUser(userModel);
            try 
                roleService.doOtherThing();
             catch (Exception e) 
                log.error(e.getMessage(), e);
            
        

    可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。

    三 其他1 大事务问题

    在使用spring事务时,有个让人非常头疼的问题,就是大事务问题。

    通常情况下,我们会在方法上@Transactional注解,填加事务功能,比如:

    @Service
    public class UserService 
        
        @Autowired 
        private RoleService roleService;
        
        @Transactional
        public void add(UserModel userModel) throws Exception 
           query1();
           query2();
           query3();
           roleService.save(userModel);
           update(userModel);
        



    @Service
    public class RoleService 
        
        @Autowired 
        private RoleService roleService;
        
        @Transactional
        public void save(UserModel userModel) throws Exception 
           query4();
           query5();
           query6();
           saveData(userModel);
        

    @Transactional注解,如果被加到方法上,有个缺点就是整个方法都包含在事务当中了。

    上面的这个例子中,在UserService类中,其实只有这两行才需要事务:

    roleService.save(userModel);
    update(userModel);

    在RoleService类中,只有这一行需要事务:

    saveData(userModel);

    现在的这种写法,会导致所有的query方法也被包含在同一个事务当中。

    如果query方法非常多,调用层级很深,而且有部分查询方法比较耗时的话,会造成整个事务非常耗时,而从造成大事务问题。

    2.编程式事务

    上面聊的这些内容都是基于@Transactional注解的,主要说的是它的事务问题,我们把这种事务叫做:声明式事务

    其实,spring还提供了另外一种创建事务的方式,即通过手动编写代码实现的事务,我们把这种事务叫做编程式事务。例如:


       @Autowired
       private TransactionTemplate transactionTemplate;
       
       ...
       
       public void save(final User user) 
             queryData1();
             queryData2();
             transactionTemplate.execute((status) => 
                addData1();
                updateData2();
                return Boolean.TRUE;
             )
       

    在spring中为了支持编程式事务,专门提供了一个类:TransactionTemplate,在它的execute方法中,就实现了事务的功能。

    相较于@Transactional注解声明式事务,我更建议大家使用,基于TransactionTemplate的编程式事务。主要原因如下:

    1. 避免由于spring aop问题,导致事务失效的问题。
    2. 能够更小粒度的控制事务的范围,更直观。

    建议在项目中少使用@Transactional注解开启事务。但并不是说一定不能用它,如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。

    1. 用IDEA调试JDK源码 (含阅读调试源码的技巧)

    2. Redis 过滤请求绝技 — 布隆过滤器与布谷鸟过滤器

    3. 阿里二面:main 方法可以继承吗?

    4. SpringBoot + SpringBatch + Quartz整合定时批量任务

    最近面试BAT,整理一份面试资料Java面试BATJ通关手册,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

    获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。

    文章有帮助的话,在看,转发吧。

    谢谢支持哟 (*^__^*)

    为啥Spring事务失效了,你踩坑了吗?

            hello,慕仔们,我们又见面了。为啥Spring事务失效了,你踩坑了吗?为啥Spring事务失效了,你踩坑了吗?前段时间小慕做的智慧社区门禁服务的业务中恰好遇上了事务失效的场景,于是就有了今天的这篇文档总结,避免让慕仔们踩坑为啥Spring事务失效了,你踩坑了吗?为啥Spring事务失效了,你踩坑了吗?为啥Spring事务失效了,你踩坑了吗?

           今天我们不聊微服务架构下的分布式事务,只谈单体应用系统下的事务,那到底哪些情况下会导致事务失效呢?咱们现在就来唠嗑唠嗑吧!

       

        

    1. 底层数据库引擎不支持事务

    以MySQL为例,MyISAM引擎不支持事务操作,InnoDB引擎支持事务,MySQL从5.5.5开始默认的引擎是InnoDB,之前的版本默认的都是MyISAM,所以这个要注意。


    2. 被@Transactional 注解修饰的方法为非public类型

    如果被@Transactional注解修饰的方法,修饰符非public或者被final修饰,则事务会失效,因为AOP没办法为这样的方法生成一个代理,自然事务就无法生效。这个在Spring的官方文档里面也有说明。


    3. 异常被吃掉了

    如果异常被 catch 住,那事务也是会失效呢,伪代码如下

    @Transactionalpublic void test(){ try{ //插入一条数据    insertData(); //更改一条数据    updateData(); }catch(Exception e){    log.error("异常被捕获,事务失效",e); }}



    4. 异常抛出类型错误

    @Transactional 注解有个属性:rollbackFor,这个属性可以设置想要回滚的异常类型,那它默认的异常类型是什么?立即回答:RuntimeException,太棒了。

    如果说我们没有设置这个属性,而且抛出的异常比这个大,那么事务就不会回滚,例如:

    @Transactionalpublic void test(){ try{ //插入一条数据    insertData(); //更改一条数据    updateData(); }catch(Exception e){ // 这个时候事务就不会回滚, throw new Exception("操作失败!"); }}


    5. 本类方法调用

    这一个应该是最容易踩坑的了,也是小慕踩坑的地方,先来看两段伪代码:

    代码一:

    @Servicepublic class TestServiceImpl implements TestService {
    public void testA() { // 查询数据,并进行一些判断 // 调用本类的另外一个方法 testB(); }
    @Transactional public void testB(Test test) { // update test }}

    小慕提问为啥Spring事务失效了,你踩坑了吗?testA()方法上面没有加 @Transactional 注解,调用有 @Transactional 注解的 testB() 方法,testB() 方法上的事务管用吗?


    代码二:

    @Servicepublic class TestServiceImpl implements TestService {
    @Transactional public void testA() { // 查询数据,并进行一些判断 // 调用另外一个方法 testB(); }
    @Transactional(propagation = Propagation.REQUIRES_NEW) public void testB(Test test) { // update test }}

    小慕提问为啥Spring事务失效了,你踩坑了吗?:在 testA() 方法上加了 @TransactionaltestB() 的注解上加了 REQUIRES_NEW 新开启一个事务,那么新开的事务管用么?


    立即推,以上两段代码的事务都不生效,答对了吧,答对的小伙伴手动@我,找我领红包,为啥Spring事务失效了,你踩坑了吗?为啥Spring事务失效了,你踩坑了吗?为啥Spring事务失效了,你踩坑了吗?

    为啥会失效,因为他们是本类的方法直接调用,这个时候会用this关键字,没有经过 Spring 的代理类去调用此方法,从而没有开启事务管理,默认只有在外部调用事务才会生效。


    总结::加事务注解的方法给本类里面的方法调用,事务不生效!


    话又说回来了,那怎么解决这个问题呢?小慕提供3种解决方案

    解法一::最直白的就是把方法拆出来,放在两个类里面(Spring推荐的一种方式);


    解法二:在类里面注入自己,用注入的对象再调用另外一个方法,这个不太优雅;


    解法三:在Spring的配置里面增加一段配置                                                    :   <aop:aspectj-autoproxyexpose-proxy="true"/>



    6、没有被spring管理

    当这个类只是一个普通类,没有被spring管理成为一个Bean对象,那它很自然的就不能使用spring提供的事务管理了,事务自然就不生效。


    半山腰总是最挤的,你得去山顶上瞅瞅,拜拜为啥Spring事务失效了,你踩坑了吗?为啥Spring事务失效了,你踩坑了吗?为啥Spring事务失效了,你踩坑了吗?

    好,今天的内容就分享到这里了,你们一定要变优秀哦,我们下期再见。


                                      历史推文


                       

                       

                       

                       

    点击蓝字