在我们平常中, 说到传播肯定是扩散, 传送或者散布的意思. 在 Spring 的事务中, 它也有传播, 而Spring 中的事务传播它是一种机制即传播机制. 这个事务传播机制和我们说的传播定义很像, 也就是说在多个包含事务的方法里相互调用时, 它们之间是如何扩散或者传递的.
一. 传播机制的作用
我们之前学事务的隔离级别中, 解决的时多个事务同时调用数据库的问题. 它保证了多个并发但独立的事务执行时是可控的( 稳定性 ).
而在事务的传播机制中, 它保证的是一个事务在多个调用方法之间的可控性( 稳定性 )
比如在我们常说的运钞过程, 在这个过程中, 运钞人员有很多环节需要执行 : 点钞 -> 运钞 -> 清点等等. 而事务的传播机制就是保证运钞这个事务在运钞过程中是可靠的. 也就是在每个运钞环节中是可靠的.
二. 事务的传播机制
在 Spring 中事务的传播机制一共有七种 :
- Propagation.REQUIRED
默认的事务传播级别, 表示如果当前存在事务, 则加入该事务; 如果当前没有事务, 则创建一个新事物
- Propagation.SUPPORTS
如果当前存在事务, 则加入事务; 如果当前不存在事务, 则以非事务的方式继续运行
- Propagation.MANDATORY
如果当前存在事务, 则加入事务; 如果当前没有事务, 则抛出异常
- Propagation.REQUIRES_NEW
表示创建一个新的事物, 如果当前存在事务则把当前事务挂起. 即无论外部是否开启事务, REQUIRES_NEW 修饰的方法会新开启自己的事务, 并且开启的事务相互之间独立, 互不干扰
- Propagation.NOT_SUPPORTED
以非事务的方式运行, 如果当前存在事务则把当前事务挂起.
- Propagation.NEVER
以非事务的方式运行, 如果当前存在事务则抛出异常
- Propagation.NESTED
如果当前存在事务, 则创建一个事务作为当前事务的嵌套事务来运行; 如果当前不存在事务, 则等价于 Propagation.REQUIRED. 即创建一个新的事物
对于上面的七种事务, 根据是否支持当前事务可以划分为三类 :
- 支持当前事务 :
- REQUIRED ( 需要有 )
- SUPPORTS ( 可以有 )
- MANDATORY ( 强制有 )
- 不支持当前事务 :
- REQUIRES_NEW
- NOT_SUPPORTED
- NEVER
- 嵌套事务 :
- NESTED
三. 支持当前事务演示( REQUIRED )
为了方便演示, 需要建立两张表. 一张用户表和一张日志表. 通过往用户表中新增数据后加入成功添加的日志信息到日志表进行演示
具体的调用过程 :
1. 创建实体类
由于后面我会采用参数传实体对象的方式进行添加, 因此需要用户表的实体对象和日志表的实体对象
用户表实体对象 :
@Data // 记得加该注解 public class UserEntity { private int id; private String username; private String password; private String photo; private LocalDateTime createtime; private LocalDateTime updatetime; private int state; }
日志表实体对象 :
@Data public class LogEntity { private int id; private String timestamp; private String message; }
2. 建立 Mybatis 接口方法
用户操作的接口方法
@Mapper public interface UserMapper { int add(UserEntity user); // 传入用户利于后期维护 }
日志操作的接口方法
@Mapper public interface LogMapper { int addLog(LogEntity log); }
3. 建立Mybatis 的 XML 方法
用户操作接口中 add 方法的具体实现
<insert id="add" > insert into userinfo(username, password) values( #{username}, #{password} ) </insert>
日志操作接口中 add 方法的具体实现
<insert id="addLog"> insert into log(message) values( #{message} ) </insert>
4. service 层调用 mapper 层
在日志操作的 LogService 中建立 add 方法( 开启事务并默认设置为 REQUIRED 传播级别 )调用 mapper 层中的 add 添加日志方法
@Service public class LogService { @Autowired private LogMapper logMapper; // @Transactional(propagation = Propagation.REQUIRED) public int add(LogEntity log) { int result = logMapper.add(log); System.out.println("添加日志条数 : " + result); int a = 10 / 0; return result; } }
在用户操作的 UserService 中建立 add 方法 ( 开启事务并默认设置为 REQUIRED 传播级别 ) 调用 mapper 层中的 add 添加用户方法, 完成后同时调用 LogService 中的 add 方法添加日志
@Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private LogService logService; @Transactional(propagation = Propagation.REQUIRED) public int add(UserEntity user) { // 添加用户 int addListUserResult = userMapper.add(user); System.out.println("添加用户条数 : " + addListUserResult); // 添加日志信息 LogEntity log = new LogEntity(); log.setMessage("添加用户成功"); logService.add(log); return addListUserResult; } }
5. controller 层调用 service 层
实际上应该创建一个 LogController 来控制 LogService, 为了方便演示直接在 UserService 中直接调用了 LogService.add( ) 方法
@RestController @RequestMapping("/user3") public class UserController3 { @Autowired private UserService userService; @RequestMapping("/add") @Transactional(propagation = Propagation.REQUIRED) public int add(String username, String password) { if (username == null || password == null || username.equals("") || password.equals("")) return 0; int result = 0; UserEntity user = new UserEntity(); user.setUsername(username); user.setPassword(password); result = userService.add(user); return result; } }
由于上面我们的三个添加方法都开启了事务, 并且设置的是默认的隔离级别, 由 UserController -> UserService -> UserMapper和LogMapper. 存在着这样的方法调用链. 根据我们的 REQUIRED 传播机制, 因为当前的 UserController 存在事务, 后面的 UserService 开启事务后会加入到当前的 UserController 的事务中, 在 UserService 里出现了异常, 会不会影响到整个调用链呢 ?
6. 预期验证
可以明确的是, 在LogService.add 方法中出现了算数异常, 那么日志肯定是会进行回滚操作的. 但是这个算数异常会不会影响到调用链上的 UserService.add 方法添加用户呢 ?
可以看到, 控制台显示用户已经添加成功了, 日志也添加了, 最后报出了算数异常而终止了
先看数据库中的日志表 : 回滚了, 并没有添加数据, 符合异常后主动回滚的预期
再来看数据库中的用户表 : 可以看到, " wangwu " 这条数据是没有成功插入进来的.
从上面的演示可以看到, 默认的传播机制 REQUIRED 调用链上的所有方法, 只要一个方法出现了异常, 那么这些方法开启的事务合并成的一个大流程的事务都会回滚. 也就是说无论是外部回滚还是内部回滚, 整个调用链都会回滚. 一荣俱荣一损俱损.
四. 不支持当前事务演示 ( REQUIRES_NEW )
将 UserController 中的添加方法以及 UserService 中添加用户方法和 LogService 中的添加日志方法开启事务后设置其事务的传播机制为 REQUIRES_NEW,
修改 UserController3 中的添加用户方法的传播机制
@RequestMapping("/add") @Transactional(propagation = Propagation.REQUIRES_NEW) public int add(String username, String password) { if (username == null || password == null || username.equals("") || password.equals("")) return 0; int result = 0; UserEntity user = new UserEntity(); user.setUsername(username); user.setPassword(password); result = userService.add(user); return result; }
修改 UserService 中的添加用户方法的传播机制
@Service public class LogService { @Autowired private LogMapper logMapper; @Transactional(propagation = Propagation.REQUIRES_NEW) public int add(LogEntity log) { int result = logMapper.add(log); System.out.println("添加日志条数 : " + result); int a = 10 / 0; return result; } }
修改 LogService 中的添加日志方法的传播机制
@Service public class LogService { @Autowired private LogMapper logMapper; @Transactional(propagation = Propagation.REQUIRES_NEW) public int add(LogEntity log) { int result = logMapper.add(log); System.out.println("添加日志条数 : " + result); int a = 10 / 0; return result; } }
对于上面的代码, 由于设置其隔离级别为 REQUIRES_NEW 表示开启新的事务, 并且和其他事务独立. 因此在 LogService 中出现算数异常会进行回滚, 但并不会影响调用链上的其他事务. 因此用户应该正常添加, 日志进行回滚.
同样调用 UserController3 里的添加方法观察
在控制台中看到, 用户已经添加, 日志也已经显示添加
数据库中查看却发现, 日志虽然回滚了, 但是用户添加也跟着回滚了. 这是为什么 ?
对于此事不符合我们设置的传播机制的预期结果而言, 是因为设置了事务注解, 此时虽然日志添加出现了算数异常, 它是一个单独的事务. 但在这一整个事务的调用链上出现了异常, 让外面的 UserController 的添加方法 和 UserService 的添加方法感知到了异常, 因此进行了回滚.
因此, 对于日志异常这个问题, 我们可以单独抛出异常处理后再来观察 REQUIRES_NEW 的传播机制
@Transactional(propagation = Propagation.REQUIRES_NEW) public int add(LogEntity log) { int result = 0; try { result = logMapper.add(log); System.out.println("添加日志条数 : " + result); int a = 10 / 0; } catch (Exception e) { // 主动对异常进行处理回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } return result; }
此时再去访问刚刚的路由方法就不在报错了, 因我们已经捕获异常并主动处理了
此时在到数据库中查看可以看到, 日志正确回滚. 但添加用户并不受到算数异常的影响正确添加了
也就是说, REQUIRES_NEW 传播机制中, 会建立新的事务, 并且这些事务都是相互独立的, 互不干扰. 也就是常说的各干各的.
五. 嵌套式事务演示
嵌套式事务就一个高中有三个年级, 一个年级又有很多个班一样. 类似于套娃的的形式.
同样的将上面的 UserController 中的添加方法以及 UserService 中添加用户方法和 LogService 中的添加日志方法开启事务后设置其事务的传播机制为 NESTED 嵌套式 ( 我就不重复演示修改了 ) . 其余代码还是刚刚演示不支持当前事务的代码
访问同样的路由方法
可以看到, 日志和用户都添加成功了. 由于在日志添加中主动进行算数异常的回滚. 因此日志是会回滚的. 看看用户添加是否也回滚了呢 ?
在数据库中可以看到, 日志正确回滚了. 但是用户也正常添加了. 这就是嵌套式事务.
肯定有细心的人发现了, 和我们上面演示的不支持当前事务的 REQUIRES_NEW 效果一样. 它两有什么区别呢 ?
六. 嵌套式事务和加入式事务的区别
区别在于 REQUIRES_NEW 无论当前存不存在事务都会建立一个新的事务, 而我们的 NESTED 如果当前存在事务则会把这个嵌套进到当前事务中, 并不会创建新事务.
**嵌套事务之所以能够实现部分事务的回滚,是因为事务中有⼀个保存点(savepoint)的概念,嵌套事务 进⼊之后相当于新建了⼀个保存点,⽽滚回时只回滚到当前保存点,因此之前的事务是不受影响的. 而 REQUIRED 是加⼊到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚, 这就是嵌套事务和加⼊事务的区别 **
- 整个事务如果执行成功, 二者结果就是一样的
- 如果事务执行到一半失败了, 那么加入事务整个事务会全部回滚. 而嵌套事务会局部回滚, 不会影响上一个方法中执行的结果.