事务Transactional注解的参数与失效场景分析

1,146 阅读5分钟

听说微信搜索《Java鱼仔》会变更强!

本文收录于JavaStarter ,里面有我完整的Java系列文章,学习或面试都可以看看哦

(一)前言

前段时间在面试的时候有个面试官问了这样一个问题,@Transactional注解能在私有方法上使用吗?如果现在这个问题问到你,你的答案是什么?

答案是在私有方法上添加@Transactional注解不能生效,平常在项目中没有用到就不会有这个印象,因此写了这篇文章来深入了解一下Transactional的用法。

(二)几个重要的参数

Transactional的作用是让被修饰的方法以事务的方式运行,所谓事务就是指这个方法中的所有持久化操作要么都进行,要么都不进行。比如订单系统中生成订单和库存删减需要是事务操作。

Transactional的使用很简单,在需要添加事务的方法处添加注解即可:

@Transactional(rollbackFor = Exception.class)
public int createOrder(String orderId) {
    //增加订单
    orderMapper.createOrder(orderId);
    //扣除账户金额
    accountMapper.updateAccount(10);
    return 1;
}

接下来介绍Transactional的重要的参数:

2.1 隔离级别 isolation

数据库中可能会遇到的三种问题:

脏读:一个事务读到另一个事务未提交的更新数据

不可重复读 : 前后多次读取,数据内容不一致

幻读 : 前后多次读取,数据总量不一致

Spring的隔离级别

  1. ISOLATION_DEFAULT: 这是一个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别.

  2. ISOLATION_READ_UNCOMMITTED:读未提交。这是事务最低的隔离级别,这种隔离级别会产生脏读,不可重复读和幻像读。

  3. ISOLATION_READ_COMMITTED:读已提交,ORACLE默认隔离级别,有幻读以及不可重复读风险。

  4. ISOLATION_REPEATABLE_READ: 可重复读,解决不可重复读的隔离级别,但还是有幻读风险。

  5. ISOLATION_SERIALIZABLE 串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,解决脏读、不可重复读和幻读。

隔离级别并非越高越好,越高的隔离级别意味着越大的资源消耗,因此需要做适当取舍。

2.2 传播属性 propagation

事务的传播属性只在Spring中存在,在数据库中的事务中不存在传播属性的说法

Spring的传播属性有以下七种:

PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务。(默认)

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

PROPAGATION_MANDATORY--支持当前事务,如果当前没有事务,就抛出异常。

PROPAGATION_REQUIRES_NEW--新建事务,如果当前存在事务,把当前事务挂起。

PROPAGATION_NOT_SUPPORTED--以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

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

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

2.2.1 重要传播属性的解释:

假设有ServiceA.MethodA(),在其内部调用ServiceB.MethodB()

PROPAGATION_REQUIRED Spring默认使用PROPAGATION_REQUIRED传播属性,当ServiceA.MethodA()运行时,开启一个事务,此时ServiceB.MethodB()方法发现已经存在一个事务,就不会再开启事务,因此不管哪一个方法报错,都会回调。

PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。当ServiceA.MethodA()运行时,开启一个事务A。当运行ServiceB.MethodB()时,把事务A挂起,然后开启事务B。就算事务A发生回滚,事务B依然能正常提交。总结起来就是:外部事务不会影响内部事务的提交和回滚。

PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。关于嵌套需要首先了解检查点的概念:当在事务中设置检查点后,如果发生回滚,会回滚到设置检查点的位置,而不是回滚整个事务。嵌套事务就使用了检查点Savepoint。当ServiceA.MethodA()运行时,开启一个事务A。当运行ServiceB.MethodB()时,开启一个子事务B,它将取得一个 savepoint. 如果这个事务B失败, 将回滚到此 savepoint,而不会影响整个事务。总结一句话就是内部事务不会影响外部事务的提交和回滚。

2.3 readOnly

默认情况下是false,如果设置为true表示该方法是可读方法,如果存在数据的修改会抛出异常。

2.4 rollbackForClassName/rollbackFor

用来指明回滚的条件是哪些异常类或者异常类名。

2.5 norollbackForClassName/noRollbackFor

用来指明不回滚的条件是哪些异常类或者异常类名。

2.6 timeout

用于设置事务处理的时间长度,防止长事件的阻塞占用系统资源。

2.7 value

指定要使用的Spring事务管理器。

(三)什么情况下Transactional会失效

3.1 修饰非public方法时

这就是开头的问题了,当用Transactional修饰非public方法时,Transactional注解是不会生效的:

3.2 在类的内部调用事务方法

如果在一个类的内部调用了事务方法,Transactional也不会生效。

@Service
public class OrderServiceImpl implements OrderService {
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int createOrder(String orderId) {
        //增加订单
        orderMapper.createOrder(orderId);
        //扣除账户金额
        accountMapper.updateAccount(10);
        return 1;
    }
    
    public void innerTransfer(){
        createOrder("111");
    }
}

3.3 捕获异常后未抛出

默认情况下只会在捕获了RuntimeException后Transactional注解才会生效,如果在代码中捕获了异常后未抛出,则Transactional失效:

@Transactional(rollbackFor = Exception.class)
public int createOrder(String orderId) {
    //增加订单
    orderMapper.createOrder(orderId);
    //扣除账户金额
    accountMapper.updateAccount(10);
    try {
        int i=1/0;
    }catch (RuntimeException e){
        e.printStackTrace();
    }
    return 1;
}

解决办法之一是在catch后再抛出一次异常:

@Transactional(rollbackFor = Exception.class)
public int createOrder(String orderId) {
    //增加订单
    orderMapper.createOrder(orderId);
    //扣除账户金额
    accountMapper.updateAccount(10);
    try {
        int i=1/0;
    }catch (RuntimeException e){
        e.printStackTrace();
        throw new RuntimeException();
    }
    return 1;
}

(四)总结

Transactional注解需要重点关注两个参数:isolation、propagation,以及三种失效情况。另外如果存在数据源的切换,单体的Transactional将失效,这个时候就需要用到分布式事务,比如seata。