面试官让我说出2种@Transactional注解的失效场景,我一口气给他说了六种

662 阅读10分钟

引言

@Transactional 注解相信大家并不陌生,平时开发中很常用的一个注解,它能保证方法内多个数据库操作要么同时成功、要么同时失败。使用@Transactional注解时需要注意许多的细节,不然你会发现@Transactional总是莫名其妙的就失效了。

下面我们从what ,where,when,四个方面彻底弄明白如何回答面试官的问题。

一、什么是事务(WHAT)

事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。

这里我们以取钱的例子来讲解:比如你去ATM机取1000块钱,大体有两个步骤:第一步输入密码金额,银行卡扣掉1000元钱;第二步从ATM出1000元钱。这两个步骤必须是要么都执行要么都不执行。如果银行卡扣除了1000块但是ATM出钱失败的话,你将会损失1000元;如果银行卡扣钱失败但是ATM却出了1000块,那么银行将损失1000元。

如何保证这两个步骤不会出现一个出现异常了,而另一个执行成功呢?事务就是用来解决这样的问题。事务是一系列的动作,它们综合在一起才是一个完整的工作单元,这些动作必须全部完成,如果有一个失败的话,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。在企业级应用程序开发中,事务管理是必不可少的技术,用来确保数据的完整性和一致性。

在我们日常开发中事务分为声明式事务和编程式事务。

编程式事务

是指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强。

编程式事务指的是通过编码方式实现事务,允许用户在代码中精确定义事务的边界。

即类似于JDBC编程实现事务管理。管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。

对于编程式事务管理,spring推荐使用TransactionTemplate。

try {
   //TODO something
    transactionManager.commit(status);
} catch (Exception e) {
   transactionManager.rollback(status);
   throw new InvoiceApplyException("异常");
}

声明式事务

管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

简单地说,编程式事务侵入到了业务代码里面,但是提供了更加详细的事务管理;

而声明式事务由于基于AOP,所以既能起到事务管理的作用,又可以不影响业务代码的具体实现。

声明式事务也有两种实现方式,一是基于TXAOP的xml配置文件方式,二种就是基于@Transactional注解了。

@GetMapping("/user")
@Transactional
public String user() {
       int insert = userMapper.insert(userInfo);
}

二、@Transactional可以在什么地方使用(WHERE)

1、@Transactional注解可以作用于哪些地方?

@Transactional 可以作用在接口类方法

  • 作用于类:当把@Transactional 注解放在类上时,表示所有该类的public方法都配置相同的事务属性信息。
  • 作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
  • 作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效
@Transactional
@RestController
@RequestMapping
public class MybatisPlusController {
   @Autowired
   private UserMapper userMapper;
   
   @Transactional(rollbackFor = Exception.class)
   @GetMapping("/user")
   public String test() throws Exception {
       User user = new User();
       user.setName("javaHuang");
       user.setAge("2");
       user.setSex("2");
       int insert = userMapper.insert(cityInfoDict);
       return insert + "";
  }
}
复制代码

2、@Transactional属性详解

propagation属性

propagation 代表事务的传播行为,默认值为 Propagation.REQUIRED,其他的属性信息如下:

  • Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务
  • Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。
  • Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
  • Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。( 当类A中的 a 方法用默认Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 )
  • Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。
  • Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。
  • Propagation.NESTED :和 Propagation.REQUIRED 效果一样。

isolation 属性

isolation :事务的隔离级别,默认值为 Isolation.DEFAULT

TransactionDefinition.ISOLATION_DEFAULT

这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是

TransactionDefinition.ISOLATION_READ_UNCOMMITTED

该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。

TransactionDefinition.ISOLATION_READ_COMMITTED

该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。TransactionDefinition.ISOLATION_REPEATABLE_READ

该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。

TransactionDefinition.ISOLATION_SERIALIZABLE

所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

timeout 属性

timeout :事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。

readOnly 属性

readOnly :指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。

rollbackFor 属性

rollbackFor :用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。

noRollbackFor属性**

noRollbackFor:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。

二、@Transactional什么时候会失效(WHEN)

面试官就直接问我有没有用过@Transactional,我肯定不能说没用过啊,十分自信的说,常用。

面试官又问我,在实际开发过程有没有遇到过@Transactional失效的情况,我肯定不能说没有啊,再次十分自信的说到,经常。

面试官一脸问号,经常???那你给我说说@Transactional在什么时候会失效呢?

下面的内容是我将我面试时说的失效场景整理了一下。

1、@Transactional 应用在非 public 修饰的方法上

如果Transactional注解应用在非public 修饰的方法上,Transactional将会失效。

之所以会失效是因为在Spring AOP 代理时,TransactionInterceptor(事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute`方法,获取Transactional 注解的事务配置信息。

protected TransactionAttribute computeTransactionAttribute(Methodmethod,
   Class<?> targetClass) {
       // Don't allow no-public methods as required.
       if (allowPublicMethodsOnly() &&!Modifier.isPublic(method.getModifiers())) {
       return null;
}

Modifier.isPublic会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。

注意:protectedprivate 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。

2、数据库引擎要不支持事务

数据库引擎要支持事务,如果是MySQL,注意表要使用支持事务的引擎,比如innodb,如果是myisam,事务是不起作用的。

3、@由于propagation 设置错误,导致注解失效

在上面解读propagation 属性的时候,我们知道

TransactionDefinition.PROPAGATION_SUPPORTS

如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

TransactionDefinition.PROPAGATION_NOT_SUPPORTED

以非事务方式运行,如果当前存在事务,则把当前事务挂起。

TransactionDefinition.PROPAGATION_NEVER

以非事务方式运行,如果当前存在事务,则抛出异常。

当我们将propagation 属性设置为上述三种时,@Transactional 注解就不会产生效果

4、rollbackFor 设置错误,@Transactional 注解失效

上述我们解读rollbackFor 属性的时候我们知道

rollbackFor 可以指定能够触发事务回滚的异常类型。

Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;

其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。

// 希望自定义的异常可以进行回滚
@Transactional(propagation= Propagation.REQUIRED,rollbackFor=MyException.class

若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。Spring源码如下:

private int getDepth(Class<?> exceptionClass, int depth) {
   if (exceptionClass.getName().contains(this.exceptionName)) {
       // Found it!
       return depth;
}
   // If we've gone as far as we can go and haven't found it...
   if (exceptionClass == Throwable.class) {
       return -1;
}
return getDepth(exceptionClass.getSuperclass(), depth + 1);
}

5、方法之间的互相调用也会导致@Transactional失效

我们来看下面的场景:

比如有一个类User,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。

那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

   //@Transactional
   @GetMapping("/user")
   private Integer A() throws Exception {
       User user = new User();
       user.setName("javaHuang");
       /**
        * B 插入字段为 topJavaer的数据
        */
       this.insertB();
       /**
        * A 插入字段为 2的数据
        */
       int insert = userMapper.insert(user);

       return insert;
  }

   @Transactional()
   public Integer insertB() throws Exception {
       User user = new User();
       user.setName("topJavaer");
       return userMapper.insert(user);
  }

6、异常被你的 catch“吃了”导致@Transactional失效

这种情况是最常见的一种@Transactional注解失效场景,

  @Transactional
  private Integer A() throws Exception {
      int insert = 0;
      try {
          User user = new User();
          user.setCityName("javaHuang");
          user.setUserId(1);
          /**
            * A 插入字段为 javaHuang的数据
            */
          insert = userMapper.insert(user);
          /**
            * B 插入字段为 topJavaer的数据
            */
          b.insertB();
      } catch (Exception e) {
          e.printStackTrace();
      }
  }

如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务就不能正常回滚,而是会报出异常

org.springframework.transaction.UnexpectedRollbackException: Transactionrolled back because it has been marked as rollback-only

解决方法:

第一声明事务的时候加上rollback='exception'

第二 cath代码块里面手动回滚

总结

@Transactional 注解我们经常使用,但是往往我们也只是知道它是一个事务注解,很多时候遇到事务注解失效的情况下,我们都是一头雾水,看不出个所以然来,花费了很长的时间都不能解决。

通过本文了解了@Transactional 注解的失效场景,在以后遇到这种情况时,基本就能一眼看破,然后摸摸自己光滑的脑门,soga,so easy!

妈妈再也不用担心我找不到自己写的bug了。