Spring Boot事务回滚

前言

我们开发系统的时候经常会遇到一些关于交易的需求,交易的过程大多数都比较繁琐(会包括修改库存、修改余额、记录交易账单等等步骤),这时候我们就不得不考虑其中的潜在风险了,比如我们在交易的过程中修改了库存(库存 -1),接下来需要进行支付操作,但是此时系统突然宕机或者网络突然中断,这也就导致我们无法完成整个交易流程,虽然用户还没付钱,但是我们的库存变少了(商家肯定就不高兴了👿),所以我们就需要用到事务回滚来解决上述的问题。

Spring Boot 事务回滚

我们有两种方式可以实现事务回滚,第一种是自动回滚,第二种是手动回滚,这两种实现方式大同小异,二者都需要使用 @Transactional 注解来实现事务回滚,下面直接上代码,看看二者之间到底哪里不一样。

在接口实现类中有一个插入会员信息的方法,咱们就对这个方法进行改造,分别实现一下自动回滚和手动回滚👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 插入会员信息
*
* @param cashierMember 会员信息
* @return 结果
*/
@Override
public int insertCashierMember(CashierMember cashierMember)
{
cashierMember.setCreateTime(DateUtils.getNowDate());
cashierMember.setCreateBy(ShiroUtils.getLoginName());
SMSUtil.sendCreateMemberMessage(cashierMember.getPhonenumber());
return cashierMemberMapper.insertCashierMember(cashierMember);
}
复制代码

自动回滚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 插入会员信息
*
* @param cashierMember 会员信息
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int insertCashierMember(CashierMember cashierMember)
{
cashierMember.setCreateTime(DateUtils.getNowDate());
cashierMember.setCreateBy(ShiroUtils.getLoginName());
SMSUtil.sendCreateMemberMessage(cashierMember.getPhonenumber());
return cashierMemberMapper.insertCashierMember(cashierMember);
}
复制代码

我们可以看到方法上增加了一个注解 @Transactional(rollbackFor = Exception.class) ,通过该注解可以对异常进行捕获,当发生异常时就可以进行回滚,从而撤销本次的入库操作。

很多方法中都会用 try-catch 对异常进行处理,如果此时在 catch 中对可能出现的异常进行了处理,但是并没有再手动抛出(throw)异常,Spring 则会认为该方法成功执行,也就不会进行回滚👇。 在这里插入图片描述 正解如下👇:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 插入会员信息
*
* @param cashierMember 会员信息
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int insertCashierMember(CashierMember cashierMember)
{
try {
cashierMember.setCreateTime(DateUtils.getNowDate());
cashierMember.setCreateBy(ShiroUtils.getLoginName());
SMSUtil.sendCreateMemberMessage(cashierMember.getPhonenumber());
return cashierMemberMapper.insertCashierMember(cashierMember);
}catch (Exception e){
System.out.println("方法出现异常:" + e);
//手动抛出异常
throw new RuntimeException();
}
}
复制代码

P.S. 如果 try-catch 语句在 finally 语句块中进行了 return 操作,那么 catch 语句块中手动抛出的异常也会被覆盖,同样不会自动回滚。

手动回滚

手动回滚的实现方式也非常简单,只需要添加一句代码即可实现👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 插入会员信息
*
* @param cashierMember 会员信息
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int insertCashierMember(CashierMember cashierMember)
{
try {
cashierMember.setCreateTime(DateUtils.getNowDate());
cashierMember.setCreateBy(ShiroUtils.getLoginName());
SMSUtil.sendCreateMemberMessage(cashierMember.getPhonenumber());
return cashierMemberMapper.insertCashierMember(cashierMember);
}catch (Exception e){
System.out.println("方法出现异常:" + e);
//实现手动回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return 0;
}
复制代码

P.S. 这里只是举个例子,手动回滚语句不一定要添加在 catch 代码块中,我们可以在任何一个地方使用手动回滚语句。需要注意的是,我们虽然可以在其他地方增加手动回滚语句,但是手动回滚语句后的代码还会继续执行,所以不建议在非 catch 代码块中使用手动回滚语句。 如果非要这么用的话,就一定要好好斟酌一下自己的业务逻辑是不是会有 BUG 了。

Spring Boot 事务回滚注意事项

这里我们再简单说几句关于 Spring Boot 事务回滚中的注意事项:

  1. 想实现回滚,首先要保证 Spring Boot 开启了事务(在启动类上增加 @EnableTransactionManagement 注解开启事务(其实 Spring Boot 默认就是开启事务的),其次就是实现回滚的方法必须是 public 的。
  2. @Transactional(rollbackFor=Exception.class) 表示的是该方法无论抛出什么异常都会进行自动回滚;如果不加 (rollbackFor=Exception.class) 的话,则代表了默认值,也就是只有当该方法抛出了非检查型异常(RuntimeException)时才会进行回滚。
  3. 由于事务的四大特性(原子性、一致性、隔离性、持久性),所以 @Transactional 一般是要加在业务层(也就是接口实现类)中。
  4. 如果将 @Transactional(rollbackFor=Exception.class) 加在了接口实现类上,那么这个类下的所有方法都将会被加上事务管理,即所有方法都会在自己出现异常时进行回滚操作。

小结

本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇‍