【Spring】或许你再也不用背Spring事务的传播行为了

339 阅读10分钟

本文主要用代码举例了Spring中事务的传播行为,共计10 例子,通过这几个例子学会分析事务传播的行为、再也不用背面经了!

传播行为总结

首先我们需要对事务的传播行为有个基本的认识,具体参见下表:

Propagation说明备注
REQUIRED需要事务,如果当前有事务就在当前事务运行、否则新建事务举例演示
REQUIRED_NEW创建一个新的事务、如果存在外部方法有事务、也会新建一个事务举例演示
NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作举例演示
SUPPORTS如果有事务则加入事务、否则以非事务方式运行
NOT_SUPPORTED以非事务方式运行、有事务则挂起事务不举例
MANDATORY如果当前存在事务、则在事务运行、否则抛出异常不举例
NEVER以非事务方式运行、如果存在事务则抛出异常不举例

事务的传播行为主要指的是一个事务对另一个事务的影响。因此可以分为外部方法有事务和没事务这两种情况,当外部方法没事务的情况、只需要考虑自身事务的影响即可。本文不做举例说明。

事先准备两张表,User 和 Device表,以对这两张表的插入做为例子。

REQUIRED

当外部方法为REQUIRED时,例子有如下几种情况:

01_required_use_case_xmind.png

首先是在外部方法没有事务的情况、这种情况下内部方法方法会新建一个事务、外部的运行情况不会影响到内部方法,不做举例。

主要验证外部方法有事务的情况。

外部方法成功内部方法也成功

  • 这种情况比较简单,你好我好大家好。

首先看代码例子、UserServiceImpl的代码如下:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
  public void insertUserByRequiredNestedRequiredNoException(User newUser) {
      log.info("预期结果:user 插入成功,device 插入成功");
      userMapper.insertUser(newUser);
      deviceService.insertDeviceByRequireNoException(
              new Device(null, "RequiredNoException", "required",
                      "normal")
      );
  }

DeviceServiceImpl的代码如下:

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
@Override
public Integer insertDeviceByRequireNoException(Device device) {
    log.info("propagation = Propagation.REQUIRED【正常运行不抛异常】");
    return deviceMapper.insertDevice(device);
}

测试代码如下:

@Test
public void test_Required_Nested_No_Exception() {
    User user = new User();
    user.setUserName("两者都能正常插入");
    user.setAge(18);
    user.setCountNumber(1);
    userService.insertUserByRequiredNestedRequiredNoException(user);
}

测试结果如下图:

02_required_user_device_insert_ok.png

可以看到正常情况下两者插入没有任何问题。

外部方法成功、内部方法抛出异常

场景代码如下:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
@Override
public void insertUserByRequiredNestedRequireThrowException(User newUser) {
​
    log.info("预期结果:user 插入失败,device 插入失败");
    userMapper.insertUser(newUser);
    deviceService.insertDeviceByRequireThrowException(
            new Device(null, "device抛出异常", "required",
                    "normal")
    );
}
​
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public Integer insertDeviceByRequireThrowException(Device device) {
    log.info("propagation = Propagation.REQUIRED【抛出异常】");
    deviceMapper.insertDevice(device);
    throw new RuntimeException("插入数据失败");
}

测试方法:

@Test
public void test_Required_Nested_Throw_Require_Exception() {
    User user = new User();
    user.setUserName("Device抛出Required异常");
    user.setAge(18);
    user.setCountNumber(1);
    userService.insertUserByRequiredNestedRequireThrowException(user);
}

运行结果如下图:User表中没有新增的数据。插入Device的方法抛出异常、本身就会回滚。因此都插入失败。

03_required_user_device[throw ex]_insert_err.png

外部方法捕获内部抛出的异常

  • 这种情况下、可能会想到由于我本身捕获了异常、导致本身的事务失效、所以User会插入成功、Device插入失败,但实际上

    由于这两个方法在同一个事务,因此要么同时成功、要么同时失败、所以运行结果是都失败!

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
@Override
public void insertUserByRequiredNestedCatchRequireException(User newUser) {
    log.info("预期结果:user 插入失败,device 插入失败");
    userMapper.insertUser(newUser);
    try {
        deviceService.insertDeviceByRequireCatchException(
                new Device(null, "User捕获device[Runtime]异常", "required",
                        "normal")
        );
    } catch (RuntimeException e) {
        log.error("插入数据失败", e);
    }
}
@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public Integer insertDeviceByRequireCatchException(Device device) {
​
    deviceMapper.insertDevice(device);
    throw new RuntimeException("插入数据失败");
​
}

运行结果下图:

04_required_user_catch[device[throw_ex]]_insert_err.png

外部方法异常

同样的分析、由于这两个方法在同一个事务里面、不可能独善其身的!

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertUserByRequiredExceptionNestedRequire(User newUser) {
​
    log.info("预期结果:user 插入失败,device 插入失败");
    userMapper.insertUser(newUser);
    deviceService.insertDeviceByNestedNoException(
            new Device(null, "Device正常运行", "required",
                    "normal")
    );
    throw new RuntimeException("插入数据失败");
​
}
Propagation方法A方法B预期结果实际运行结果
REQUIRED外部正常运行内部方法正常运行两张表正常插入正常运行
外部正常、不捕获B抛出的异常内部抛出异常均失败插入失败
外部正常、捕获B抛出的异常内部抛出异常均失败插入失败
外部出现异常内部正常/异常均失败插入失败

得出如下结论:在外部有事务的情况下、required 会进入外部事务、因此他们在同一个事务里面、要么都成功、要么都失败。

注意:当内部方法抛出的异常被本身捕获之后没有抛出时,此时对于内部方法的事务控制已经失效了,内外都不会回滚。

REQUIRED_NEW

  • 无论外部方法有没有事务、都会新建一个事务。

外部方法有事务、内部方法抛出异常

分析:内部方法出现异常、本身就会回滚、外部方法感知到异常、也会回滚。

测试方法:

@Test
public void test_Require_Nested_Require_New_Throw_Exception() {
    User user = new User();
    user.setUserName("二者插入失败");
    user.setAge(18);
    user.setCountNumber(1);
    userService.insertUserByRequireNestedRequireNewThrowException(user);
}

场景代码如下:

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertUserByRequireNestedRequireNewThrowException(User newUser) {
    log.info("预期结果:user 插入失败,device 插入失败");
    userMapper.insertUser(newUser);
    deviceService.insertDeviceByRequireNewThrowException(
            new Device(null, "都插入失败", "required_new",
                    "normal")
    );
}
​
@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public Integer insertDeviceByRequireNewThrowException(Device device) {
    log.info("propagation = Propagation.REQUIRES_NEW【抛出异常】");
    deviceMapper.insertDevice(device);
    throw new RuntimeException("插入数据失败");
}

运行结果:

05_required_new_device_throw_err.png

可以看到结果符合分析预期

外部方法有事务、内部方法抛出异常被捕获

两个方法不在同一个事务里面、外部方法捕获异常导致本身事务失效、因此运行结果为user插入成功、device插入失败

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertUserByRequireNestedRequireNewCatchException(User newUser) {
    log.info("预期结果:user 插入成功,device 插入失败");
    userMapper.insertUser(newUser);
    try {
        deviceService.insertDeviceByRequireNewCatchException(
                new Device(null, "UserService捕获抛出的异常", "required_new",
                        "normal")
        );
    } catch (RuntimeException e) {
        log.error("插入数据失败", e);
    }
}

测试代码:

@Test
public void test_Require_Nested_Require_New_Catch_Exception() {
    User user = new User();
    user.setUserName("User插入成功,Device插入失败");
    user.setAge(18);
    user.setCountNumber(1);
    userService.insertUserByRequireNestedRequireNewCatchException(user);
}

外部方法有事务且异常、内部方法没有异常

由于两个方法在不同的事务、且不是嵌套关系、外部失败、外部回滚、内部方法的事务没有感知

@Test
public void test_Require_Exception_Nested_Require_New_No_Exception() {
    User user = new User();
    user.setUserName("user:runtime-ex,device insert ok");
    user.setAge(18);
    user.setCountNumber(1);
    userService.insertUserByRequireExceptionNestedNoException(user);
}

场景代码如下:

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertUserByRequireExceptionNestedNoException(User newUser) {
    log.info("预期结果:user 插入失败,device 插入成功");
    userMapper.insertUser(newUser);
    deviceService.insertDeviceByRequireNewNoException(
            new Device(null, "NestedNoException", "required",
                    "normal")
    );
    throw new RuntimeException("插入数据失败");
}
@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public Integer insertDeviceByRequireNewNoException(Device device) {
    log.info("propagation = Propagation.REQUIRES_NEW【正常运行不抛异常】");
    return deviceMapper.insertDevice(device);
}
外部方法A内部方法B理论分析实际结果
外部方法有事务且正常、没有捕获内部抛出的异常抛出异常内部方法在一个新的事务里面,发生异常回滚,外部方法感知到异常,也回滚两者都没有插入成功
外部方法有事务且正常、捕获抛出的异常抛出异常内部方法在一个新的事务里面,发生异常回滚,外部方法捕获异常、事务没有感知到异常user成功、device失败
外部方法有事务且异常、内部方法没有异常正常外部事务发生异常、导致回滚,内部方法正常没有感知到外部异常,因此插入正常user失败、device正常

总结:REQUIRE_NEW需要新建一个事务、外部事务在编码层面可以对其感知、理解为是否try catch,当在事务方法里面进行异常捕获而没有抛出异常时,于这个方法而言、事务控制本身已经失效了。新建的事务无法感知外部事务状态。

NESTED

MySQL并不支持在事务里面新建一个事务、是通过安全点的方式模拟嵌套事务、这样可以回滚到安全点。我们可以把这种传播行为理解为在事务里面新开了一个子事务。

外部正常、内部抛出异常

内部方法抛出异常、本身就会回滚、外部方法感知到异常之后、也会回滚。

@Test
public void test_Require_Nested_Nested_Throw_Exception (){
​
    User user = new User();
    user.setUserName("二者均失败: 没有捕获异常");
    user.setAge(18);
    user.setCountNumber(1);
    userService.insertUserByRequireNestedNestedThrowException(user);
}

对应的方法代码如下:

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertUserByRequireNestedNestedThrowException(User newUser) {
    log.info("预期结果:user 插入失败,device 插入失败");
    userMapper.insertUser(newUser);
​
    deviceService.insertDeviceByNestedThrowException(
            new Device(null, "Nested Throw Exception", "nested",
                    "normal")
    );
}
@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
public Integer insertDeviceByNestedThrowException(Device device) {
    log.info("propagation = Propagation.NESTED【抛出异常】");
    deviceMapper.insertDevice(device);
    throw new RuntimeException("插入数据失败");
}

外部正常、内部抛出异常被捕获

内部异常会导致本身回滚、外部捕获异常、事务管理没有感知到异常、因此User成功、Device插入失败。

@Test
public void test_Require_Nested_Nested_Catch_Exception (){
​
    User user = new User();
    user.setUserName("User插入成功,Nested插入失败");
    user.setAge(18);
    user.setCountNumber(1);
    userService.insertUserByRequireNestedNestedCatchException(user);
}

对应的场景代码:

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertUserByRequireNestedNestedCatchException(User newUser) {
    log.info("预期结果:user 插入成功,device 插入失败");
    userMapper.insertUser(newUser);
​
    try {
        deviceService.insertDeviceByNestedCatchException(
                new Device(null, "User Catch Exception", "nested",
                        "normal")
        );
    } catch (RuntimeException e) {
        log.error("插入数据失败", e);
    }
}
​
@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
public Integer insertDeviceByNestedCatchException(Device device) {
    log.info("propagation = Propagation.NESTED 捕获");
    deviceMapper.insertDevice(device);
    throw new RuntimeException("插入数据失败");
}

外部异常内部正常

这里可以理解为外部事务回滚、因此不管内部事务是那种运行状态、都会回滚。

@Test
public void test_Require_Exception_Nested_Nested_No_Exception (){
​
    User user = new User();
    user.setUserName("二者均失败: UserService出现异常");
    user.setAge(18);
    user.setCountNumber(1);
    userService.insertUserByRequireExceptionNestedNestedNoException(user);
}

场景代码如下:

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertUserByRequireExceptionNestedNestedNoException(User newUser) {
​
    log.info("预期结果:user 插入失败,device 插入失败");
    userMapper.insertUser(newUser);
    deviceService.insertDeviceByNestedNoException(
            new Device(null, "NestedNoException", "nested",
                    "normal")
    );
    throw new RuntimeException("插入数据失败");
}
​
@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
public Integer insertDeviceByNestedNoException(Device device) {
    log.info("propagation = Propagation.NESTED【正常运行不抛异常】");
    return deviceMapper.insertDevice(device);
}
外部方法内部方法分析结果
外部正常、没有捕获异常抛出异常外部感知到异常、都回滚都失败
外部正常、捕获内部抛出异常抛出异常由于异常被捕获、事务管理没有感知到异常、外部成功内部失败user 成功、device 失败
外部异常、内部正常外部异常回滚、导致"嵌套"在内的子事务也回滚都失败

在分析了上述几种事务的传播行为之后、其余几种就更简单、如下对support分析。

support

  • 如果有事务则加入事务、否则以非事务方式运行

先分析一下,如果外部方法有事务、则加入事务运行,可以知道这两个方法在同一个事务里面。要么同时成功、要么同时失败、如果外部方法捕获了内部方法抛出的异常、外部内部方法都失败、

外部异常的情况:

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertUserNestedSupports() {
    userMapper.insertUser(new User(null, "User Supports", 1,
            1));
    deviceService.insertDeviceBySupports(
            new Device(null, "Device Supports", "supports",
                    "normal")
    );
    throw new RuntimeException("插入数据失败");
}
​
@Transactional(rollbackFor = Exception.class, propagation = Propagation.SUPPORTS)
@Override
public Integer insertDeviceBySupports(Device device) {
    log.info("propagation = Propagation.SUPPORTED");
    return deviceMapper.insertDevice(device);
}

外部捕获异常的情况:运行结果都失败

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void insertUserNestedSupports() {
  userMapper.insertUser(new User(null, "User Supports", 1,
          1));
  try {
      deviceService.insertDeviceBySupports(
              new Device(null, "Device Supports", "supports",
                      "normal")
      );
  }catch (RuntimeException e){
      e.printStackTrace();
  }
}
@Transactional(rollbackFor = Exception.class, propagation = Propagation.SUPPORTS)
@Override
public Integer insertDeviceBySupports(Device device) {
    log.info("propagation = Propagation.SUPPORTED");
    deviceMapper.insertDevice(device);
    throw new RuntimeException("插入数据失败");
}

同理可以分析其他几种传播行为。