本文主要用代码举例了Spring
中事务的传播行为,共计10 例子,通过这几个例子学会分析事务传播的行为、再也不用背面经了!
传播行为总结
首先我们需要对事务的传播行为有个基本的认识,具体参见下表:
Propagation | 说明 | 备注 |
---|---|---|
REQUIRED | 需要事务,如果当前有事务就在当前事务运行、否则新建事务 | 举例演示 |
REQUIRED_NEW | 创建一个新的事务、如果存在外部方法有事务、也会新建一个事务 | 举例演示 |
NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作 | 举例演示 |
SUPPORTS | 如果有事务则加入事务、否则以非事务方式运行 | |
NOT_SUPPORTED | 以非事务方式运行、有事务则挂起事务 | 不举例 |
MANDATORY | 如果当前存在事务、则在事务运行、否则抛出异常 | 不举例 |
NEVER | 以非事务方式运行、如果存在事务则抛出异常 | 不举例 |
事务的传播行为主要指的是一个事务对另一个事务的影响。因此可以分为外部方法有事务和没事务这两种情况,当外部方法没事务的情况、只需要考虑自身事务的影响即可。本文不做举例说明。
事先准备两张表,User 和 Device表,以对这两张表的插入做为例子。
REQUIRED
当外部方法为REQUIRED
时,例子有如下几种情况:
首先是在外部方法没有事务的情况、这种情况下内部方法方法会新建一个事务、外部的运行情况不会影响到内部方法,不做举例。
主要验证外部方法有事务的情况。
外部方法成功内部方法也成功
- 这种情况比较简单,你好我好大家好。
首先看代码例子、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);
}
测试结果如下图:
可以看到正常情况下两者插入没有任何问题。
外部方法成功、内部方法抛出异常
场景代码如下:
@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
的方法抛出异常、本身就会回滚。因此都插入失败。
外部方法捕获内部抛出的异常
这种情况下、可能会想到由于我本身捕获了异常、导致本身的事务失效、所以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("插入数据失败");
}
运行结果下图:
外部方法异常
同样的分析、由于这两个方法在同一个事务里面、不可能独善其身的!
@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("插入数据失败");
}
运行结果:
可以看到结果符合分析预期
外部方法有事务、内部方法抛出异常被捕获
两个方法不在同一个事务里面、外部方法捕获异常导致本身事务失效、因此运行结果为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("插入数据失败");
}
同理可以分析其他几种传播行为。