A 80% of people can't tell the knowledge point - Spring transaction propagation behavior

Free book delivery: juejin.cn/post/712095…

I have interviewed many people, and most of them can talk about the four characteristics and isolation levels of transactions, but when asked about Spring's propagation behavior, basically no one can say one, two, three.

We all know that a transaction either succeeds or fails. But when several transactions cooperate to complete a complex task, it is not easy to cut across the board like this. We need to specify which tasks need to be rolled back together according to the relationship between tasks, and which tasks will not affect other tasks even if they fail. To solve this problem, you need to understand the propagation behavior of transactions. There are seven transaction propagation behaviors in Spring, as shown in the following table:

Transaction propagation behavior type illustrate
PROPAGATION_REQUIRED If there is no current transaction, create a new transaction, if there is already a transaction, join the transaction. This is the most common choice.
PROPAGATION_SUPPORTS Support the current transaction, if there is no current transaction, it will be executed in a non-transactional mode.
PROPAGATION_MANDATORY Use the current transaction, or throw an exception if there is no current transaction.
PROPAGATION_REQUIRES_NEW Create a new transaction. If there is a current transaction, suspend the current transaction.
PROPAGATION_NOT_SUPPORTED Execute the operation in a non-transactional manner, suspending the current transaction if there is a current transaction.
PROPAGATION_NEVER Executes non-transactionally and throws an exception if a transaction currently exists.
PROPAGATION_NESTED If a transaction currently exists, execute within a nested transaction. If there are no current transactions, then do something similar to PROPAGATION_REQUIRED.

Spring can set different propagation behavior strategies through the propagation attribute of the @Transactional annotation. Spring provides an enumeration class Propagation for this, the source code is as follows:

package org.springframework.transaction.annotation;
​
import org.springframework.transaction.TransactionDefinition;
​
public enum Propagation {
​
    /**
     * 需要事务,它是默认传播行为,如果当前存在事务,就沿用当前事务,
     * 否则新建一个事务运行内部方法
     */
    REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
​
    /**
     * 支持事务,如果当前存在事务,就沿用当前事务,
     * 如果不存在,则继续采用无事务的方式运行内部方法
     */
    SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
​
    /**
     * 必须使用事务,如果当前没有事务,则会抛出异常,
     * 如果存在当前事务,则沿用当前事务
     */
    MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
​
    /**
     * 无论当前事务是否存在,都会创建新事务运行方法,
     * 这样新事务就可以拥有新的锁和隔离级别等特性,与当前事务相互独立
     */
    REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
​
    /**
     * 不支持事务,当前存在事务时,将挂起事务,运行方法
     */
    NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
​
    /**
     * 不支持事务,如果当前方法存在事务,则抛出异常,否则继续使用无事务机制运行
     */
    NEVER(TransactionDefinition.PROPAGATION_NEVER),
​
    /** 
     * 在当前方法调用内部方法时,如果内部方法发生异常,
     * 只回滚内部方法执行过的 SQL ,而不回滚当前方法的事务
     */
    NESTED(TransactionDefinition.PROPAGATION_NESTED);
​
        ......
​
}

接下来我们通过对其中三种最常用的(REQUIRED、REQUIRES_NEW、NESTED)策略进行对比来更深入的理解。以下测试均在外部方法开启事务的情况下进行,因为在外部没有事务的情况下,三者都会新建事务,效果一样。

REQUIRED

当内部方法的事务传播行为设置为 REQUIRED 时,内部方法会加入外部方法的事务。我们在 UserServiceImpl 中添加如下方法:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
​
    @Autowired
    private UserMapper mapper;
    
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void addWithRequired(User user) {
        mapper.insert(user);
    }
​
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void addWithRequiredAndException(User user) {
        mapper.insert(user);
        throw new RuntimeException();
    }
}

创建 TransactionServiceImpl 类,并添加如下方法:

@Slf4j
@Service
public class TransactionServiceImpl implements TransactionService {
​
    @Autowired
    private UserService userService;
​
    @Override
    public void noTransaction_required_required_externalException() {
        
        User xiaoShui = new User().setName("小水");
        User xiaoJing = new User().setName("小镜");
        userService.addWithRequired(xiaoShui);
        userService.addWithRequired(xiaoJing);
        throw new RuntimeException();
        
    }
​
    @Override
    public void noTransaction_required_requiredException() {
        
        User xiaoShui = new User().setName("小水");
        User xiaoJing = new User().setName("小镜");
        userService.addWithRequired(xiaoShui);
        userService.addWithRequiredAndException(xiaoJing);
        
    }
​
    @Override
    @Transactional
    public void transaction_required_required_externalException() {
        
        User xiaoShui = new User().setName("小水");
        User xiaoJing = new User().setName("小镜");
        userService.addWithRequired(xiaoShui);
        userService.addWithRequired(xiaoJing);
        throw new RuntimeException();
        
    }
​
    @Override
    @Transactional
    public void transaction_required_requiredException() {
        
        User xiaoShui = new User().setName("小水");
        User xiaoJing = new User().setName("小镜");
        userService.addWithRequired(xiaoShui);
        userService.addWithRequiredAndException(xiaoJing);
        
    }
​
​
    @Override
    @Transactional
    public void transaction_required_requiredException_try() {
        
        User xiaoShui = new User().setName("小水");
        User xiaoJing = new User().setName("小镜");
        userService.addWithRequired(xiaoShui);
        try {
            userService.addWithRequiredAndException(xiaoJing);
        } catch (Exception e) {
            log.error("发生异常,事务回滚!");
        }
        
    }
}

结果分析如下表所示:

方法 结果 分析
noTransaction_required_required_externalException 小水和小镜均成功入库 外部方法未开启事务,所以所有插入操作均未受到外部异常影响
noTransaction_required_requiredException 小水入库,小镜未入库 外部方法未开启事务,内部方法事务各自独立,互不影响,「小镜」的插入方法发生异常回滚,但「小水」的插入方法不受影响
transaction_required_required_externalException 小水和小镜均未入库 外部方法开启事务,所有内部方法均加入外部方法的事务中。而外部方法发生异常,所以导致所有操作都发生回滚
transaction_required_requiredException 小水和小镜均未入库 外部方法开启事务,所有内部方法均加入外部方法的事务中。由于「小镜」的插入方法发生异常,此时所有方法都处于同一个事务中,所以导致所有操作都发生回滚
transaction_required_requiredException_try 小水和小镜均未入库 外部方法开启事务,所有内部方法均加入外部方法的事务中。由于「小镜」的插入方法发生异常,此时所有方法都处于同一个事务中,即使发生异常的部分被 try-catch 住,所有操作仍然会回滚

前面四种情况都比较好理解,很多人不能理解最后一种情况:我都 try-catch 了你还想怎样?这里的关键点在于所有方法都处于同一个事务中,此时「小镜」的插入方法发生异常,那么这个方法所在的事务就会被 Spring 设置为 rollback 状态。因为异常被 catch 了,所以外部方法执行完要进行 commit 操作,这时却发现当前事务已经处于 rollback 状态了,虽然它不知道哪里出了问题,但也只能听从指挥,回滚所有操作了。

PS:由于外部方法不开启事务的情况,在每种传播行为下结果都是类似的,所以后面不再给出示例。

REQUIRES_NEW

当内部方法的传播行为设置为 REQUIRES_NEW 时,内部方法会先将外部方法的事务挂起,然后开启一个新的事务 。在 UserServiceImpl 中添加如下方法:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
​
    ...
​
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addWithRequiredNew(User user) {
        
        mapper.insert(user);
        
    }
​
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addWithRequiredNewAndException(User user) {
        
        mapper.insert(user);
        throw new RuntimeException();
        
    }
}

在 TransactionServiceImpl 中添加如下方法:

@Slf4j
@Service
public class TransactionServiceImpl implements TransactionService {
​
    ...
​
    @Override
    @Transactional
    public void transaction_required_requiredNew_externalException() {
        
        User xiaoShui = new User().setName("小水");
        User xiaoJing = new User().setName("小镜");
        userService.addWithRequired(xiaoShui);
        userService.addWithRequiredNew(xiaoJing);
        throw new RuntimeException();
        
    }
​
    @Override
    @Transactional
    public void transaction_required_requiredNew_requiredNewException() {
        
        User xiaoShui = new User().setName("小水");
        User xiaoJing = new User().setName("小镜");
        User shuiJing = new User().setName("水镜");
        userService.addWithRequired(xiaoShui);
        userService.addWithRequiredNew(xiaoJing);
        userService.addWithRequiredNewAndException(shuiJing);
        
    }
​
    @Override
    @Transactional
    public void transaction_required_requiredNewException_try() {
        
        User xiaoShui = new User().setName("小水");
        User xiaoJing = new User().setName("小镜");
        User shuiJing = new User().setName("水镜");
        userService.addWithRequired(xiaoShui);
        userService.addWithRequiredNew(xiaoJing);
        try {
            userService.addWithRequiredNewAndException(shuiJing);
        } catch (Exception e) {
            log.error("发生异常,事务回滚!");
        }
        
    }
}

结果分析如下表所示:

方法 结果 分析
transaction_required_requiredNew_externalException 小水未入库,小镜入库 外部方法开启事务,「小水」的插入方法和外部方法在同一个事务中,跟随外部方法发生回滚;「小镜」的插入方法开启一个独立的新事务,不受外部方法异常的影响
transaction_required_requiredNew_requiredNewException 小水未入库,小镜入库,水镜未入库 外部方法开启事务,「水镜」的插入方法开启一个独立的新事务,因为发生异常,所以自己回滚了;「水镜」的异常没有做处理,因此会被外部方法感知到,「小水」的插入方法和外部方法在同一个事务中,跟随外部方法发生回滚;「小镜」的插入方法也会开启一个独立的新事务,因此不会受到任何方法的影响,成功入库
transaction_required_requiredNewException_try 小水和小镜入库,水镜未入库 外部方法开启事务,「水镜」的插入方法开启一个独立的新事务,因为发生异常,所以自己回滚了;「水镜」的异常被 try-catch 处理了,其他方法正常提交「小水」和「小镜」成功入库

NESTED

当内部方法的传播行为设置为 NESTED 时,内部方法会开启一个新的嵌套事务(子事务)。在 UserServiceImpl 中添加如下方法:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
​
    ...
​
    @Override
    @Transactional(propagation = Propagation.NESTED)
    public void addWithNested(User user) {
        
        mapper.insert(user);
        
    }
​
    @Override
    @Transactional(propagation = Propagation.NESTED)
    public void addWithNestedAndException(User user) {
        
        mapper.insert(user);
        throw new RuntimeException();
        
    }
}

在 TransactionServiceImpl 中添加如下方法:

@Slf4j
@Service
public class TransactionServiceImpl implements TransactionService {
​
    ...
​
    @Override
    @Transactional
    public void transaction_nested_nested_externalException() {
        
        User xiaoShui = new User().setName("小水");
        User xiaoJing = new User().setName("小镜");
        userService.addWithNested(xiaoShui);
        userService.addWithNested(xiaoJing);
        throw new RuntimeException();
        
    }
​
    @Override
    @Transactional
    public void transaction_nested_nestedException() {
        
        User xiaoShui = new User().setName("小水");
        User xiaoJing = new User().setName("小镜");
        userService.addWithNested(xiaoShui);
        userService.addWithNestedAndException(xiaoJing);
        
    }
​
    @Override
    @Transactional
    public void transaction_nested_nestedException_try() {
        
        User xiaoShui = new User().setName("小水");
        User xiaoJing = new User().setName("小镜");
        User shuiJing = new User().setName("水镜");
        userService.addWithRequired(xiaoShui);
        userService.addWithNested(xiaoJing);
        try {
            userService.addWithNestedAndException(shuiJing);
        } catch (Exception e) {
            log.error("发生异常,事务回滚!",e);
        }
        
    }
}

结果分析如下表所示:

方法 结果 分析
transaction_nested_nested_externalException 小水和小镜均未入库 外部方法开启事务,内部方法开启各自的子事务,外部方法发生异常,主事务回滚,子事务跟随主事务回滚
transaction_nested_nestedException 小水和小镜均未入库 外部方法开启事务,内部方法开启各自的子事务,「小镜」的插入方法发生异常回滚自己的子事务;「小镜」的异常没有做处理,因此会被外部方法感知到,「小水」的插入方法在外部方法的子事务中,所以跟随主事务回滚
transaction_nested_nestedException_try 小水和小镜入库,水镜未入库 外部方法开启事务,「小镜」和「水镜」开启各自的子事务,「小水」加入外部方法的事务。「水镜」的插入方法发生异常回滚自己的子事务;「水镜」的异常被 try-catch 处理了,其他方法正常提交「小水」和「小镜」成功入库

每个 NESTED 事务执行前会将当前操作保存下来,叫做 savepoint (保存点),如果当前 NESTED 事务执行失败,则回滚到之前的保存点,保存点使得子事务的回滚不对主事务造成影响。NESTED 事务在外部事务提交以后自己才会提交。

总结

REQUIRES_NEW 最为简单,不管当前有无事务,它都会开启一个全新事务,既不影响外部事务,也不会影响其他内部事务,真正的井水不犯河水,坚定而独立。

When there is no external transaction, REQUIRED will open an independent new transaction and will not affect other transactions at the same level; and when there is an external transaction, it will live and die with the external transaction.

NESTED has the same effect as REQUIRED in the absence of an outer transaction; while in the presence of an outer transaction, when the outer transaction rolls back, it creates a nested transaction (subtransaction). When the external transaction is rolled back, the sub-transaction will be rolled back; but the rollback of the sub-transaction will not affect the external transaction and other transactions at the same level.

More exclusive and exciting content in my new book " Spring Boot Fun Practical Lessons ".

Guess you like

Origin juejin.im/post/7120900365658095624