「Spring」事务失效的场景

Posted

tags:

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

参考技术A

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

Spring 事务生效的前提是所连接的数据库要支持事务,如果底层的数据库引擎都不支持事务,则Spring的事务肯定会失效。

例如: MySQL 用的不是 InnoDB 引擎,而是用的 MyISAM 存储引擎。

2.事务方法未被 Spring 容器管理

如果事务方法所在的类没有加载到 Spring IOC 容器中,也就是说,事务方法所在的类没有被 Spring 容器管理,则Spring事务会失效。

例如:你的方法所在类没有加@Component或者@Service注解。

3.方法没有被 public 修饰

如果事务所在的方法没有被 public 修饰,此时 Spring 的事务也会失效。

4.同一类中方法之间直接的调用

例如:如果同一个类中有两个方法分别为 A 和 B,方法 A 没有添加事务注解,而方法 B 添加了 @Transactional 事务注解,此时方法 A 直接调用方法 B,则方法 B 的事务会失效。

5.未配置事务管理器

如果在项目中没有配置 Spring 的事务管理器,即使使用了 Spring 的事务管理功能,Spring 的事务也不会生效。

例如:对于 SpringBoot 项目来说,导入了 mybatis 的 starter 依赖后,SpringBoot 会自动注入DataSourceTransactionManager 事务管理器,这样我们就可以直接用 @Transactional 注解使用事务了。

6.事务传播类型不支持事务

如果方法的事务传播类型为不支持事务的传播类型,则该方法的事务在 Spring 中会失效。

例如: A 方法的事务传播类型为 NOT_SUPPORTED,不支持事务,此时用带事务的方法 B 去调用 A 方法,则 A 方法的事务失效。

7.进行异常捕捉却没有抛出

比如对某一个新增数据代码段进行 try catch 异常,而 catch 里没有向外抛出异常,此时 spring 事务无法回滚。

8.错误的标注异常类型

如果在 @Transactional 注解中标注的异常类型不是我们抛出的异常类型,则Spring事务的回滚会失效。

例如: Spring 中默认回滚的异常类型为 RuntimeException,如果此时你抛出的异常是 Exception,那么Spring 事务中无法捕获到 Exception 异常,则事务回滚会失效。

9.开启多线程

开启一个线程去执行数据库操作,多线程内的方法将不被 spring 事务控制。

例如:一个带事务的方法 A 中开启线程去执行同类中的一个 insert 方法,即使这个操作失败了,也不会回滚 A 中的其他数据库操作。

注意:如果把 insert 方法提出到一个新的类中,加入事务注解,就能成功的把 insert 方法加入到 spring 事务管理中。但是使用多线程事务的情况下,如果想进行回滚,比较麻烦,因为我们感知不到线程中方法执行的异常。

聊聊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」事务失效的场景的主要内容,如果未能解决你的问题,请参考以下文章

    Spring 事务失效的7种场景

    Spring 事务失效的八种场景

    导致spring事务失效的几种场景

    项目中Spring事务失效的场景问题排查

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

    259期面试官:Spring事务失效的场景有哪些?如何解决?