目录
1. 什么是事务的传播机制
事务传播机制: 多个事务方法存在调⽤关系时, 事务是如何在这些方法间进行传播的。
比如有两个方法A, B都被 @Transactional 修饰, A方法调用B方法,A方法运⾏时, 会开启⼀个事务。当A调⽤B时, B方法本⾝也有事务, 此时B方法运⾏时, 是加⼊A的事务, 还是创建⼀个新的事务呢?
这个就涉及到了事务的传播机制。
事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题:
⽽事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题:
2. 事务的传播机制有哪些?
@Transactional 注解⽀持事务传播机制的设置, 通过 propagation 属性来指定传播行为。
@Transactional(propagation = Propagation.MANDATORY)
public Integer insertUser(String userName, String password) {
// 代码略
return 1;
}
Spring 事务传播机制有以下 7 种:
- Propagation.REQUIRED: 默认的事务传播级别。如果当前存在事务, 则加⼊该事务。如果当前没有事务, 则创建⼀个新的事务。
- Propagation.SUPPORTS: 如果当前存在事务,则加⼊该事务。如果当前没有事务, 则以非事务的⽅式继续运行。
- Propagation.MANDATORY:强制性。如果当前存在事务,则加⼊该事务。如果当前没有事务,则抛出异常。
- Propagation.REQUIRES_NEW: 创建⼀个新的事务。如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法都会新开启⾃⼰的事务,且开启的事务相互独立, 互不⼲扰。
- Propagation.NOT_SUPPORTED:以非事务⽅式运⾏,如果当前存在事务, 则把当前事务挂起(不用)。
- Propagation.NEVER:以非事务⽅式运⾏,如果当前存在事务, 则抛出异常。
- Propagation.NESTED:如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏。如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
比如⼀对新人要结婚了, 关于是否需要房⼦:
- Propagation.REQUIRED : 需要有房⼦。如果你有房, 我们就⼀起住, 如果你没房, 我们就⼀起买房。(如果A当前存在事务, B就用A的事务. 如果A没有事务, B就创建⼀个新的事务)
- Propagation.SUPPORTS : 可以有房⼦。如果你有房, 那就⼀起住。如果没房, 那就租房。(如果A存在事务, B就用A的事。 如果A没有事务, B就以非事务的⽅式继续运⾏)
- Propagation.MANDATORY : 必须有房⼦。要求必须有房, 如果没房就不结婚。(如果A存在事务, B就加⼊该事务。如果A没有事务, B就抛出异常)
- Propagation.REQUIRES_NEW : 必须买新房。不管你有没有房, 必须要两个⼈⼀起买房。即使有房也不住. (如果A有事务,就把事务挂起,创建⼀个新的事务。如果A没有事务, 也创建事务)
- Propagation.NOT_SUPPORTED : 不需要房子。不管你有没有房, 我都不住, 必须租房.(不论A是否存在事务,B都以非事务方式运行)
- Propagation.NEVER : 不能有房⼦。(以非事务方式运行。A不能有事务,如果有事务, 就抛出异常)
- Propagation.NESTED : 如果你没房, 就⼀起买房。如果你有房, 我们就以房⼦为根据地,做点小⽣意。(如果当前存在事务, 则创建⼀个事务作为当前事务的嵌套事务来运⾏。如果当前没有事务, 则该取值等价于PROPAGATION_REQUIRED )
3. Spring事务的传播机制的使用和场景演示
准备阶段:创建两张表user_info和log_info。
@RestController
@RequestMapping("/proga")
public class ProgaController {
@Autowired
private LogService logService;
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/p1")
public String p1(String userName, String password) {
userService.insertUser(userName, password);
LogInfo logInfo = new LogInfo();
logInfo.setUserName(userName);
logInfo.setOp("用户主动注册");
logService.insertLog(logInfo);
return "用户注册成功";
}
}
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional
public Integer insertUser(String userName, String password) {
return userInfoMapper.insertUser(userName, password);
}
}
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional
public Integer insertLog(LogInfo logInfo) {
return logInfoMapper.insertLog(logInfo);
}
}
@Mapper
public interface UserInfoMapper {
@Insert("insert into user_info(user_name, `password`) values(#{userName},#{password})")
Integer insertUser(String userName, String password);
}
@Mapper
public interface LogInfoMapper {
@Insert("insert into log_info(user_name, op) values(#{userName}, #{op})")
Integer insertLog(LogInfo logInfo);
}
数据库初始数据:
我们现在要实现一个功能,用户注册功能,做如下操作:
- 用户注册,用户表插⼊⼀条数据
- 记录操作日志, 日志表插⼊⼀条数据(出现异常)
3.1. REQUIRED(加入事务)
启动程序,观察 propagation = Propagation.REQUIRED(可以不写,默认的事务传播级别) 的执⾏结果:
访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
观察后端日志,事务提交成功:
查看数据库user_info,数据插入成功:
查看数据库log_info,数据插入成功:
如果其中一个事务有异常:
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
观察后端日志,事务没有提交,事务全部进行了回滚:
查看数据库user_info,没有新的数据插入:
查看数据库log_info,没有新的数据插入:
流程描述:
- p1 ⽅法开始事务
- ⽤户注册, 插⼊⼀条数据 (执行成功) (和p1 使⽤同⼀个事务)
- 记录操作⽇志, 插⼊⼀条数据(出现异常, 执行失败) (和p1 使⽤同⼀个事务)
- 因为步骤3出现异常, 事务回滚. 步骤2和3使⽤同⼀个事务, 所以步骤2的数据也回滚了
总结:
两个事务全部成功,事务提交。
一个及以上失败,事务全部回滚。
3.2. REQUIRES_NEW(新建事务)
将上述UserService 和LogService 中相关⽅法事务传播机制改为Propagation.REQUIRES_NEW
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
观察后端日志,事务全部提交成功:
查看数据库user_info,有新的数据插入:
查看数据库log_info,有新的数据插入:
如果其中一个事务有异常:
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
观察后端日志,发现⽤户数据插⼊成功了,事务提交,日志表数据插入失败,事务回滚:
查看数据库user_info,有新的数据插入:
查看数据库log_info,没有数据插入:
总结:
两个事务全部成功,事务提交。
其中一个失败,两个事务之间不互相影响。
3.3. NEVER (不支持当前事务, 抛异常)
修改UserService 中对应⽅法的事务传播机制为 Propagation.NEVER:
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
观察后端日志,程序执⾏报错, 没有数据插⼊:
3.4. NESTED(嵌套事务)
将上述UserService 和LogService 中相关⽅法事务传播机制改为 Propagation.NESTED:
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
观察后端日志,事务全部提交:
查看数据库user_info,有新的数据插入:
查看数据库log_info,有新的数据插入:
如果其中一个事务有异常:
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
观察后端日志,事务全部回滚:
查看数据库user_info,没有新的数据插入:
查看数据库log_info,没有新的数据插入:
流程描述:
- Controller 中p1 ⽅法开始事务
- UserService ⽤户注册, 插⼊⼀条数据 (嵌套p1事务)
- LogService 记录操作⽇志, 插⼊⼀条数据(出现异常, 执⾏失败) (嵌套p1事务, 回滚当前事务, 数据添加失败)
- 由于是嵌套事务, LogService 出现异常之后, 往上找调⽤它的⽅法和事务, 所以⽤户注册也失败了
- 最终结果是两个数据都没有添加
p1事务可以认为是⽗事务, 嵌套事务是⼦事务. ⽗事务出现异常, ⼦事务也会回滚, ⼦事务出现异常, 如果不进⾏处理, 也会导致⽗事务回滚。
总结:
两个事务全部成功,事务提交。
其中一个失败,两个事务都回滚。
3.5. NESTED和REQUIRED 有什么区别?
- 对于NESTED来说,修改LogService,对异常进行捕获:
启动程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
观察后端日志,事务全部提交:
查看数据库user_info,数据插入成功:
查看数据库log_info,数据插入成功:
我们继续修改,对LogService手动添加事务回滚:
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
观察后端日志,只回滚当前事务:
查看数据库user_info,数据插入成功:
查看数据库log_info,没有新数据添加:
总结:LogService 中的事务已经回滚, 但是嵌套事务不会回滚嵌套之前的事务, 也就是说嵌套事务可以实现部分事务回滚。
- 同样,对于REQUIRED来说,修改LogService,对异常进行捕获:
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
观察后端日志,事务全部提交:
查看数据库user_info,有新的数据插入:
查看数据库log_info,有新的数据插入:
我们继续修改,对LogService手动添加事务回滚:
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
观察后端日志,事务全部回滚:
查看数据库user_info,没有新数据插入:
查看数据库log_info,没有新数据插入:
**总结:**REQUIRED 如果回滚就是回滚所有事务, 不能实现部分事务的回滚。 (因为属于同⼀个事务)。
NESTED和REQUIRED区别:
- 整个事务如果全部执⾏成功, 二者的结果是⼀样的。
- 如果事务⼀部分执⾏成功, REQUIRED加⼊事务会导致整个事务全部回滚。NESTED嵌套事务可以实现局部回滚, 不会影响上⼀个⽅法中执⾏的结果。
嵌套事务之所以能够实现部分事务的回滚, 是因为事务中有⼀个保存点(savepoint)的概念, 嵌套事务进⼊之后相当于新建了⼀个保存点, ⽽回滚时只回滚到当前保存点。
REQUIRED 是加⼊到当前事务中, 并没有创建事务的保存点, 因此出现了回滚就是整个事务回滚, 这就是嵌套事务和加⼊事务的区别。