目录

前言

NESTED 与 REQUIRES_NEW 之间到底有怎么样的区别?

StackOverflow 找到的一个 Answer,是这么回答两者之间的区别的:

PROPAGATION_REQUIRES_NEW 为给定范围启动一个新的、独立的“内部”事务。此事务将完全独立于外部事务提交或回滚,具有自己的隔离作用域、自己的锁集等。外部事务将在内部事务开始时挂起,并在内部事务完成后恢复。

PROPAGATION_NESTED 启动一个“嵌套”事务,这是现有事务的真正子事务。将发生的情况是在嵌套事务开始时采用保存点。如果嵌套事务失败,我们将回滚到该保存点。嵌套事务是外部事务的一部分,因此它只会在外部事务结束时提交。

代码验证一

这部分实测一的代码在外层函数调用内层函数时,没有进行try catch操作。这样内层方法throw的异常会直接传递到外层方法中。此时,使用 NESTED 和 REQUIRES_NEW 的区别是什么?

部分代码

@Service
public class DBServiceImpl implements DBService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private DBAnotherService dbAnotherService;

    @Override
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public void outterMethod() {

        // 插入一个用户
        User user = new User();
        user.setUsername("methodA-username");
        user.setPassword("methodA-password");
        userMapper.insert(user);

        // 调用 DBServiceB 的 methodB() 方法
        dbAnotherService.innerMethod();

        // 模拟抛出运行时异常
//        throw new RuntimeException();
    }

}

@Service
public class DBAnotherServiceImpl implements DBAnotherService {

    @Autowired
    private UserMapper userMapper;

    @Override
    // 使用  REQUIRES_NEW
//    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    // 使用 NESTED
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
    public void innerMethod() {

        User user = new User();
        user.setUsername("methodB-username");
        user.setPassword("methodB-password");
        userMapper.insert(user);

        // 模拟抛出运行时异常
//        throw new RuntimeException();
    }
}

// 测试代码
@SpringBootTest
class DBExampleTest {

    @Autowired
    public UserMapper userMapper;

    /**
     * 每次测试前,先清空数据库表
     */
    @BeforeEach
    public void setUp() {
        userMapper.delete(null);
    }

    @Autowired
    public DBService dbService;

    @Test
    void test() {
        dbService.outterMethod();
    }
}

测试思路

在测试时,保持外层函数的事务传播属性为 propagation = Propagation.REQUIRED 不变,这也是Spring 默认事务传播属性。

通过改变内层函数的事务传播属性 propagation,以及抛出异常的位置,进行观察。主要分为以下四种场景:

  • 场景一:设置内层方法的 propagation = Propagation.REQUIRES_NEW,在外层函数的最后加上 throw new RuntimeException();
  • 场景二:设置内层方法的 propagation = Propagation.REQUIRES_NEW,在内层函数的最后加上 throw new RuntimeException();
  • 场景三:设置内层方法的 propagation = Propagation.NESTED,在外层函数的最后加上 throw new RuntimeException();
  • 场景四:设置内层方法的 propagation = Propagation.NESTED,在内层函数的最后加上 throw new RuntimeException();

观察结果

  • 场景一的结果:内层函数的事务正常commit,新增一条数据;外层事务rollback,没有新增数据。
  • 场景二的结果:内层函数的事务rollback,没有新增数据;外层事务rollback,没有新增数据。
  • 场景三的结果:内层函数的事务rollback,没有新增数据;外层事务rollback,没有新增数据。
  • 场景四的结果:内层函数的事务rollback,没有新增数据;外层事务rollback,没有新增数据。

代码验证一总结

  • 无论是Propagation.REQUIRES_NEW,还是Propagation.NESTED,只要是内部事务抛出异常数据库都会回滚;
  • Propagation.REQUIRES_NEW是一个全新开启的事务,即使外部事务抛出异常发生数据库回滚,也不影响内部事务的提交。
  • Propagation.NESTED则更像是一个事务的子事务,受外部事务的影响。

代码验证二

如果外层方法在调用内层方法时,对内层方法做了try catch,捕获到异常之后,不做任何处理。这时使用 NESTED 和 REQUIRES_NEW 的区别是什么?

部分代码

代码部分唯一不同的地方就是在外层方法调用内层方法的地方,加上了 try catch 语句捕获异常,但不进行处理。测试代码也单独加了一个。

// 其他部分代码跟实测一的代码一致,此处省略
    try {
        // 调用 DBServiceB 的 methodB() 方法
        dbAnotherService.innerMethod();
    } catch (Exception e) {
        log.error("将异常吞掉,不向上抛出异常");
    }

// 测试代码
    /**
     * 对异常进行了 try catch 的测试
     */
    @Test
    void testForTryCatch() {
        dbService.outterMethodForTryCatch();
    }

测试思路

这里的测试思路与代码验证一的测试思路一样。

观察结果

  • 场景一的结果:内层函数的事务正常commit,新增一条数据;外层事务rollback,没有新增数据。
  • 场景二的结果:内层函数的事务rollback,没有新增数据;外层事务正常commit,新增一条数据。
  • 场景三的结果:内层函数的事务rollback,没有新增数据;外层事务rollback,没有新增数据。
  • 场景四的结果:内层函数的事务rollback,没有新增数据;外层事务正常commit,新增一条数据。

代码验证二总结

在 try catch 消化了内部异常的情况下:

  • 无论是Propagation.REQUIRES_NEW,还是Propagation.NESTED,内部事务抛出异常,都不会影响外层事务正常commit。
  • 在内层函数的事务为 Propagation.REQUIRES_NEW 时,内外层方法出现异常事务独立commit和rollback,互不影响。
  • 在内层函数的事务为 Propagation.NESTED 时,外层事务出现异常,会将内层事务产生的commit进行rollback,而内层事务rollback,不会影响外层事务commit。

应用场景

此处示例来自 Spring事务传播行为

假设我们有一个注册的方法,方法中调用添加积分的方法,如果我们希望添加积分不会影响注册流程(即添加积分执行失败回滚不能使注册方法也回滚),我们会这样写:

   @Service
   public class UserServiceImpl implements UserService {
        
        @Transactional
        public void register(User user){
                   
            try {
                membershipPointService.addPoint(Point point);
            } catch (Exception e) {
                // 不向上抛出异常
            }
            //省略...
        }
        //省略...
   }

我们还规定注册失败要影响addPoint()方法(注册方法回滚添加积分方法也需要回滚),那么addPoint()方法就需要这样实现:

   @Service
   public class MembershipPointServiceImpl implements MembershipPointService{
        
        @Transactional(propagation = Propagation.NESTED)
        public void addPoint(Point point){
                   
            try {
                recordService.addRecord(Record record);
            } catch (Exception e) {
                // 不向上抛出异常
            }
            //省略...
        }
        //省略...
   }

我们注意到了在addPoint()中还调用了addRecord()方法,这个方法用来记录日志。他的实现如下:

   @Service
   public class RecordServiceImpl implements RecordService{
        
        @Transactional(propagation = Propagation.NOT_SUPPORTED)
        public void addRecord(Record record){
            // 记录逻辑...
        }
        //省略...
   }

可以注意到addRecord()方法中propagation = Propagation.NOT_SUPPORTED,因为对于日志无所谓精确,可以多一条也可以少一条。

所以addRecord()方法本身和外围addPoint()方法抛出异常都不会使 register() 方法回滚,并且addRecord()方法抛出异常也不会影响外围addPoint()方法的执行。

通过这个例子可以对事务传播行为的使用有更加直观的认识,通过各种属性的组合确实能让业务实现更加灵活多样。

源码链接

本文代码放在了下面的Github仓库地址中:

https://github.com/MaoPingZou/spring-propagation-test/tree/master

Reference

  1. Propagation.NESTED 和Propagation.REQUIRES_NEW的区别
  2. spring事务传播行为