今天在查看以前写的代码时,看到了事务的使用,感觉自己对这一块并不是特别清晰,所以就系统的学习了一下。在学习过程中发现很多地方自己以前理解的还是有点不对,所以记录一下学习笔记,希望帮助到大家。
一、事务传播行为
备注:因为除了PROPAGATION_REQUIRES_NEW和PROPAGATION_NESTED,其他的都不是特别难以理解,所以我这里就只对这两个做了一下代码实例。
当事务方法被另外一个事务方法调用时,必须指定事务应该如何传播,例如,方法可能继续在当前事务中执行,也可以开启一个新的事务,在自己的事务中执行。
声明式事务的传播行为可以通过 @Transactional 注解中的 propagation 属性来定义,比如说:
@Transactional(propagation = Propagation.REQUIRED)
public void savePosts(PostsParam postsParam) {
}
TransactionDefinition 一共定义了 7 种事务传播行为,其中PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW 两种传播行为是比较常用的。
PROPAGATION_REQUIRED
这也是 @Transactional 默认的事务传播行为,指的是如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。更确切地意思是:如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。如果外部方法开启事务并且是 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务都需要回滚。也就是说如果a方法和b方法都添加了注解,在默认传播模式下,a方法内部调用b方法,会把两个方法的事务合并为一个事务。
PROPAGATION_REQUIRES_NEW
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法都会开启自己的事务。举例来说:
@Resource
ApplicationContext applicationContext;
@Resource
private IUserService userService;
@GetMapping("insert")
@Transactional(rollbackFor = Exception.class)
public Result insertUser(){
User user = new User();
user.setPhone("11111801061").setNickName("111");
userService.save(user);
UserController bean = applicationContext.getBean(UserController.class);
bean.saveAnother();
int k = 10/0;
return Result.ok();
}
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
public void saveAnother() {
User user = new User();
user.setPhone("11111801062").setNickName("222");
userService.save(user);
}
在上面这个例子中,首先是insertUser()方法会开启一个事务,然后在调用到saveAnother()方法时,因为该方法的事务行为是Propagation.REQUIRES_NEW,所以它会在新开启一个事务,并且将原来的事务挂起。当saveAnother()方法的事务结束以后,insertUser()方法的事务恢复开始继续执行。
在上面的例子中,因为saveAnother()方法是独自开启了一个新的事务,所以它并不受原来事务的影响,它在事务提交以后才会恢复insertUser()的事务,所以在insertUser()中虽然抛出了异常,但是此时saveAnother()事务早已提交,所以它并不受insertUser()中抛出异常的影响。
但是如果反过来,把代码改成下面这种情况
那么此时,因为saveAnother()在执行完成以后会将异常抛给insertUser(),所以insertUser()会将自己的事务进行回滚。这也就是很多博客说的A不会影响B但是B会影响A。
PROPAGATION_NESTED
如果当前存在事务,就在当前事务内执行;否则,就执行与 PROPAGATION_REQUIRED 类似的操作。我们用下面的例子来说明:
@GetMapping("insert")
@Transactional(rollbackFor = Exception.class)
public Result insertUser(){
User user = new User();
user.setPhone("11111801061").setNickName("111");
userService.save(user);
UserController bean = applicationContext.getBean(UserController.class);
bean.saveAnother();
return Result.ok();
}
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
public void saveAnother() {
User user = new User();
user.setPhone("11111801062").setNickName("222");
int k = 10/0;
userService.save(user);
}
在上面的代码中,因为saveAnother()方法事务行为是Propagation.NESTED,所以它会嵌套在insertUser()方法的事务中执行。其实这个嵌套应该就类似于mysql中的savepoint,比如现在saveAnother()方法中产生了异常,那么就会将事务回滚到当前事务开始的地方。但是!!!!此时saveAnother()方法并不会提交自己的事务!!它的事务会跟insertUser()一起提交!!这也是PROPAGATION_REQUIRES_NEW的本质区别,它是相当于开了一个子事务,跟随父级事务一起提交,并不会先提交。所以当它发生了异常以后,会先回滚自己的方法到达,将数据恢复到它自己事务开启之前的状态。
如果父级对其异常进行了捕获,那么父级事务可以正常提交。
如果父级事务接到异常不进行捕获那么自己也会回滚自己的事务。
如果两个方法都没问题,那么最终数据会被一起commit。
如果在insertUser()中发生了异常,因为此时saveAnother()方法事务还没有提交,那么最终就会导致再最后两个事务都会被回滚。这也就是大家说的A会影响B但是B不会影响A。但是A还是能够正常接收到B的异常的,处理情况下才不会影响,不处理两个会一起回滚。
可能对mysql不太熟悉的朋友对这个概念有点陌生。其实就是在一个事务中假如有很多修改的地方,我可以在某一个修改点增加一个savepoint,然后在这个savepoint之后再去执行其余的数据修改。如果修改发生了问题我可以选择回滚到某一个savepoint,在这个savepoint之前修改的数据并不会被回滚。如果还是不太明白的话可以搜几篇文章看一下,应该就会很清晰了。
PROPAGATION_SUPPORTS
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
PROPAGATION_NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
PROPAGATION_NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。
二、事务失效的情况
我觉得失效这个事情也很好理解,在排除数据库不支持的情况。其他情况应该就是你拿到的并不是spring aop生成的代理对象。熟悉spring IOC 的应该都清楚在spring创建Bean时如果检测到有事务就会提前进行aop生成代理对象。用于执行事务。所以如果说我们事务失效,那么首先判定你当前拿到的是不是代理对象。
举一个比较常见的例子:
@GetMapping("insert")
@Transactional(rollbackFor = Exception.class)
public Result insertUser(){
User user = new User();
user.setPhone("11111801061").setNickName("111");
userService.save(user);
this.saveAnother();
return Result.ok();
}
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
public void saveAnother() {
User user = new User();
user.setPhone("11111801062").setNickName("222");
int k = 10/0;
userService.save(user);
}
在执行过程中下个断点很容易就能看到
可以看到我们拿到的就是当前对象本身,并不是代理对象,所以此时它调用的方法上如果有事务,事务就会失效。
所以此时可以通过ApplicationContext或者注入(解决方案可以自己看一下,有很多种方式)来获取当前对象的代理类。
可以看到这样拿到的就是当前类的代理类了,所以此时再去执行,被调用的方法事务就会生效。
1.数据库不支持事务。
Spring事务生效的前提是所连接的数据库要支持事务,如果底层的数据库都不支持事务,则Spring的事务肯定会失效。例如,如果使用的数据库为MySQL,并且选用了MyISAM存储引擎,则Spring的事务就会失效。
2.事务方法未被Spring管理。
如果事务方法所在的类没有加载到Spring IOC容器中,也就是说,事务方法所在的类没有被Spring管理,则Spring事务会失效。
3.方法没有被public修饰。
如果事务所在的方法没有被public修饰,此时Spring的事务会失效。
4.同一类中方法调用。
如果同一个类中的两个方法分别为A和B,方法A上没有添加事务注解,方法B上添加了 @Transactional事务注解,方法A调用方法B,则方法B的事务会失效。
5.未配置事务管理器。
如果在项目中没有配置Spring的事务管理器,即使使用了Spring的事务管理功能,Spring的事务也不会生效。
6.方法的事务传播类型不支持事务。
如果内部方法的事务传播类型为不支持事务的传播类型,则内部方法的事务在Spring中会失效。
7.不正确的捕获异常。
不正确的捕获异常也会导致Spring的事务失效。
8.错误的标注异常类型。
如果在@Transactional注解中标注了错误的异常类型,则Spring事务的回滚会失效。
希望对大家能够有所帮助!如有问题欢迎交流