「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」
基础概念
事务
事务(Transaction)是访问和更新数据库的程序执行单元,事务中可能包含一个或多个 sql 语句,这些 sql 语句要么全部执行,要么全部不执行。
事务的传播行为
事务的传播行为解决的问题是,当一个事务方法被另一个事务方法调用时,这个事务方法如何运行。如下:
A.class
public void A() {
B();
do something...
}
B.class
@Transactional(Propagation = xxx)
public void B() {
do something...
}
事务方法 methodA 中调用了 事务方法 methodB,Propagation = xxx 决定了 methodB 的事务传播行为,是在调用者 methodA 的事务中运行,还是自己开启一个新的事务。注意,methodA 并没有开启事务,事务传播行为修饰的方法并不是必须要在开启事务的方法中调用。
spring 中七种事务传播行为
| 传播行为 | 说明 |
|---|---|
| Propagation.REQUIRED | 如果当前存在事务,则使用该事务。 如果当前没有事务,则创建一个新的事务。 |
| Propagation.REQUIRES_NEW | 创建一个新的事务,如果当前有事务,暂停当前的事务。 |
| Propagation.SUPPORTS | 如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。 |
| Propagation.MANDATORY | 如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。 |
| Propagation.NOT_SUPPORTED | 以非事务的方式运行,如果当前存在事务,暂停当前的事务。 |
| Propagation.NEVER | 以非事务的方式运行,如果当前存在事务,则抛出异常。 |
| Propagation.NESTED | 如果当前存在事务,则创建一个嵌套事务来运行,如果当前没有事务,行为等同于PROPAGATION_REQUIRED |
代码验证
事务的传播行为常用的有 Propagation.REQUIRED 和 Propagation.REQUIRES_NEW,这篇文章验证这两种事务传播行为。
首先,创建三个service。UserService 里有个总的方法,changeAmount 方法来修改数量,具体操作分为两步, LogService 的 addLog 方法记录日志, AmountAddService 的 addAmount 方法修改数量。
UserService.class
public void changeAmount(Integer id, Integer amount) {
logService.addLog(id, amount);
amountAddService.addAmount(id, amount);
}
LogService.class
@Transactional(propagation = Propagation.REQUIRED)
public void addLog(Integer id, Integer amount) {
logdao.insert(id,amount);
}
AmountAddService.class
@Transactional(propagation = Propagation.REQUIRED)
public void addAmount(Integer id, Integer amount) {
addDao.updateAmountAdd(id, amount);
}
现在,总方法是没有加事务注解的,添加日志方法和修改数量方法都加了事务注解,且事务传播行为都是默认的 Propagation.REQUIRED。
下面是创建的两张表,要修改数量的 user 表,和记录日志的 log 表。user 表中 id 为1对应的数量初始值是100,我们代码的目标操作就是将这个100加50,log 表初始没有数据。
好了,现在我们可以执行修改数量的总方法,调用如下。
@Autowired
private UserService userService;
public void test {
userService.changeAmount(1,50);
}
主方法和两个子方法都没有抛出异常,查看数据库,id 为 1的数量变为150,插入了一条日志。
以上是程序正常执行的结果,接下来,通过在主方法或子方法中抛出运行时异常,以及修改事务传播行为等操作,来验证事务注解的作用。注意:每次实验前都将代码恢复原样,再做修改,方便控制变量,并且将数据库恢复到初始值,方便观察结果。
实验一
第一次实验我们让主方法抛出运行时异常。注意:现在主方法是没有加事务注解的。
UserService.class
public void changeAmount(Integer id, Integer amount) {
logService.addLog(id, amount);
amountAddService.addAmount(id, amount);
throw new RuntimeException(“抛异常。。。”);
}
执行完数据库变化如下。
可以看到,添加日志和添加数量都操作成功了。主方法的异常不会影响到子方法,子方法中各自创建了自己的事务并成功提交。
实验二
这次我们让添加数量的子方法抛出异常,其他代码不改变。
AmountAddService.class
@Transactional(propagation = Propagation.REQUIRED)
public void addAmount(Integer id, Integer amount) {
addDao.updateAmountAdd(id, amount);
throw new RuntimeException(“抛异常。。。”);
}
数据没有修改,但日志插入成功了。说明在外围方法不添加事务注解时,添加日志和修改数据两个子方法分别有自己的事务,一个抛异常回滚了不影响另一个。
实验一和实验二验证了:Propagation.REQUIRED 传播行为,如果当前没有事务,则创建一个新的事务。
实验三
如果外围方法有事务呢?子方法会加入已有的事务吗?
现在我们让主方法也加上 @Transactional 注解,并且抛出异常。注意:接下去的实验中主方法都添加了事务。
UserService.class
@Transactional
public void changeAmount(Integer id, Integer amount) {
logService.addLog(id, amount);
amountAddService.addAmount(id, amount);
throw new RuntimeException(“抛异常。。。”);
}
数据没有修改,日志也没有添加。说明主方法中开启了事务,子方法会加入主方法的事务,主方法抛异常了,所有的操作都回滚。
实验四
现在我们让添加日志的子方法抛异常。
LogService.class
@Transactional(propagation = Propagation.REQUIRED)
public void addLog(Integer id, Integer amount) {
logdao.insert(id,amount);
}
可以看到,和实验三一样,两个子方法都回滚了。主方法开启了事务,被 Propagation.REQUIRES 修饰的子方法加入主方法的事务,只要其中一个出现异常,主方法感知到异常使整个事务回滚。
实验三和实验四我们验证了:Propagation.REQUIRED 传播行为,如果当前存在事务,则使用该事务。
实验五
接下来我们来看 Propagation.REQUIRES_NEW 的事务传播行为。
让主方法抛异常,修改数量的子方法的事务传播行为修改为 Propagation.REQUIRES_NEW。
UserService.class
@Transactional
public void changeAmount(Integer id, Integer amount) {
logService.addLog(id, amount);
amountAddService.addAmount(id, amount);
throw new RuntimeException(“抛异常。。。”);
}
AmountAddService.class
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addAmount(Integer id, Integer amount) {
addDao.updateAmountAdd(id, amount);
}
执行后发现,修改数量的方法没有回滚,修改数量成功,插入日志的操作回滚了。说明被 Propagation.REQUIRES_NEW 修饰的子方法创建了新的事务,主方法事务回滚了不影响修改数量的子方法。
实验五验证了:Propagation.REQUIRES_NEW 传播行为,不管当前有没有事务,都会创建一个新的事务。
实验六
如果主方法不抛异常,Propagation.REQUIRES_NEW 修饰的子方法中抛异常了会是什么情况?
AmountAddService.class
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addAmount(Integer id, Integer amount) {
addDao.updateAmountAdd(id, amount);
throw new RuntimeException(“抛异常。。。”);
}
一开始我的直观想法是,修改数量的子方法创建了新的事务,它抛异常,那么修改数量失败,插入日志是在主方法的事务中,和修改数量的子方法不是一个事务,所以插入日志成功。真是这样吗?
结果是,修改数量和插入日志都失败了。因为修改数量的子方法中抛异常了,主方法感知到异常,所以主方法的事务也发生了回滚。