这几天在工作过程中,出现了 @Transactional 注解没有生效的情况。于是打算系统的整理一下。

1 什么是 @Transactional

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

2 如何使用 @Transactional

@Transactional(rollbackFor=Exception.class)

        如果加了这个注解,那么加上这个注解的方法抛出异常,就会回滚,数据库里面的数据也会回滚。

        需要注意的是,在@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚。

        运行时异常和其他异常的关系可以通过下图来理解

springboo transactionTemplate 传播方式设置_回滚

3 事务的传播行为

         当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring定义了七种传播行为:

传播行为

含义

TransactionDefinition.PROPAGATION_REQUIRED

如果当前没有事务,就新建一个事务,如果已经存在一个事务,则加入到这个事务中。这是最常见的选择。

TransactionDefinition.PROPAGATION_SUPPORTS

支持当前事务,如果当前没有事务,就以非事务方式执行。

TransactionDefinition.PROPAGATION_MANDATORY

表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常

TransactionDefinition.PROPAGATION_REQUIRED_NEW

表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。

TransactionDefinition.PROPAGATION_NOT_SUPPORTED

表示该方法不应该运行在事务中。如果当前存在事务,就把当前事务挂起。

TransactionDefinition.PROPAGATION_NEVER

表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常

TransactionDefinition.PROPAGATION_NESTED

如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

4 事务的隔离规则

        隔离级别定义了一个事务可能受其他并发事务影响的程度。

       在实际开发过程中,我们绝大部分的事务都是有并发情况。下多个事务并发运行,经常会操作相同的数据来完成各自的任务。在这种情况下可能会导致以下的问题:

  • 脏读(Dirty reads)—— 事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。
  • 不可重复读(Nonrepeatable read)—— 事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
  • 幻读(Phantom read)—— 系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

        不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

        咱们已经知道了在并发状态下可能产生: 脏读、不可重复读、幻读的情况。因此我们需要将事务与事务之间隔离。根据隔离的方式来避免事务并发状态下脏读、不可重复读、幻读的产生。Spring中定义了五种隔离规则:

隔离级别

含义

脏读

不可重复读

幻读

TransactionDefinition.ISOLATION_DEFAULT

使用后端数据库默认的隔离级别

TransactionDefinition.ISOLATION_READ_UNCOMMITTED

允许读取尚未提交的数据变更(最低的隔离级别)

TransactionDefinition.ISOLATION_READ_COMMITTED

允许读取并发事务已经提交的数据

TransactionDefinition.ISOLATION_REPEATABLE_READ

对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改

TransactionDefinition.ISOLATION_SERIALIZABLE

最高的隔离级别,完全服从ACID的隔离级别,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的

5 @Transcational 失效的场景与原因

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

        事务拦截器在目标方法执行前后进行拦截,内部会调用方法来获取Transactional 注解的事务配置信息,调用前会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。

5.2 @Transactional 注解属性 rollbackFor 设置错误

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

5.3 同一个类中方法调用,导致@Transactional失效

@Service
public class TestServiceImpl implements TestService {
    
    @Autowired
    private TestMapper testMapper;
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void insertTestInnerInvoke() {
        //正常public修饰符的事务方法
        int re = testMapper.insert(new Test(10,20,30));
        if (re > 0) {
            throw new NeedToInterceptException("need intercept");
        }
        testMapper.insert(new Test(210,20,30));
    }

    @Override
    public void testInnerInvoke(){
        //类内部调用@Transactional标注的方法。
        insertTestInnerInvoke();
    }

}
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {

   @Autowired
   private TestServiceImpl testService;

   /**
    * 测试内部调用@Transactional标注方法
    */
   @Test
   public void  testInnerInvoke(){
       //测试外部调用事务方法是否正常
      //testService.insertTestInnerInvoke();
       //测试内部调用事务方法是否正常
      testService.testInnerInvoke();
   }
}

        经过测试可以知道,外部调用事务方法可以正常回滚,但是如果改用内部调用事务方法,无法正常回滚。

        事务管理是基于动态代理对象的代理逻辑实现的,那么如果在类内部调用类内部的事务方法,这个调用事务方法的过程并不是通过代理对象来调用的,而是直接通过 this 对象来调用方法,绕过的代理对象,肯定就是没有代理逻辑了。

5.4 异常被处理了

@Service
public class TestServiceImpl implements TestService {
    
    @Autowired
    private TestMapper testMapper;

    @Transactional(rollbackFor = Exception.class)
    public void insertTestCatchException() {
        try {
            int re = testMapper.insert(new Test(10,20,30));
            if (re > 0) {
                //运行期间抛异常
                throw new NeedToInterceptException("need intercept");
            }
            testMapper.insert(new Test(210,20,30));
        }catch (Exception e){
            System.out.println("i catch exception");
        }
    }
    
}

                查看源码,只有捕获到异常才会回滚,如果catch 处理了,就捕获不到异常,事务就不会回滚。

//开启事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
     // This is an around advice: Invoke the next interceptor in the chain.
     // This will normally result in a target object being invoked.
     //反射调用业务方法
     retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
     // target invocation exception
     //异常时,在catch逻辑中回滚事务
     completeTransactionAfterThrowing(txInfo, ex);
     throw ex;
}

5.5 数据库引擎不支持事务

        开启事务的前提就是需要数据库的支持,我们一般使用的Mysql引擎时支持事务的,所以一般不会遇到这个情况。

5.6 开启多线程任务时,事务管理会受到影响

        因为线程不属于spring托管,故线程不能够默认使用spring的事务,也不能获取spring注入的bean在被spring声明式事务管理的方法内开启多线程,多线程内的方法不被事务控制。