spring中12种@Transactional的失效场景(小结)
作者:superjava_ 发布时间:2022-04-26 00:06:40
数据库事务是后端开发中不可缺少的一块知识点。Spring为了更好的支撑我们进行数据库操作,在框架中支持了两种事务管理的方式: 编程式事务声明式事务
日常我们进行业务开发时,基本上使用的都是声明式事务,即为使用@Transactional
注解的方式。
常规使用时,Spring能帮我们很好的实现数据库的ACID (这里需要注意哦,Spring只是进行了编程上的事务,最终数据上的事务还是有数据库实现的) 。
但是,只要是人写的代码,就一定会有Bug。
如果我们不了解@Transactional
的失效场景或者说踩坑点,那么在业务开发的过程中总是会出现一些匪夷所思的Bug。
同样它也是面试时高频的考点哦!
本文将罗列@Transactional
的失效场景,并分析其失效原因。
一、失效场景集一:代理不生效
Spring中对注解解析的尿性都是基于代理的,如果目标方法无法被Spring代理到,那么它将无法被Spring进行事务管理。
Spring生成代理的方式有两种:
基于接口的JDK * ,要求目标代理类需要实现一个接口才能被代理
基于实现目标类子类的CGLIB代理
Spring在2.0之前,目标类如果实现了接口,则使用JDK * 方式,否则通过CGLIB子类的方式生成代理。
而在2.0版本之后,如果不在配置文件中显示的指定spring.aop.proxy-tartget-class
的值,默认情况下生成代理的方式为CGLIB,如下图
顺着代理的思路,我们来看看哪些情况会因为代理不生效导致事务管控失败。
(1)将注解标注在接口方法上
@Transactional
是支持标注在方法与类上的。一旦标注在接口上,对应接口实现类的代理方式如果是CGLIB,将通过生成子类的方式生成目标类的代理,将无法解析到@Transactional
,从而事务失效。
这种错误我们还是犯得比较少的,基本上我们都会将注解标注在接口的实现类方法上,官方也不推荐这种。
(2)被final、static关键字修饰的类或方法
CGLIB是通过生成目标类子类的方式生成代理类的,被final、static修饰后,无法继承父类与父类的方法。
(3)类方法内部调用
事务的管理是通过代理执行的方式生效的,如果是方法内部调用,将不会走代理逻辑,也就调用不到了。
例如
在createUser中调用了内部方法createUser1,并且createUser1方法上设置了事务传播策略为:REQUIRES_NEW,但是因为是内部直接调用,createUser1不能不代理处理,无法进行事务管理。在createUser1方法抛出异常后就插入数据失败了。
但是这种操作在我们业务开发的过程中貌似还挺常见的,怎么样才能保证其成功呢?
方式1:新建一个Service,将方法迁移过去,有点麻瓜。
方式2:在当前类注入自己,调用createUser1时通过注入的userService调用
方式3:通过AopContext.currentProxy()获取代理对象
道理类似于方式2,就是为了通过代理来访问内部方法
(4)当前类没有被Spring管理
这个没什么好说的,都没有被Spring管理成为IOC容器中的一个bean,更别说被事务切面代理到了。
这种Bug看上去比较蠢,但没准真的有人犯错。
二、失效场景集二:框架或底层不支持的功能
这类失效场景主要聚焦在框架本身在解析@Transactional
时的内部支持。如果使用的场景本身就是框架不支持的,那事务也是无法生效的。
(1)非public修饰的方法
我们在标有@Transactional
的任意方法上打个断点,在idea内能看到事务切面点如下图所示
点击去这个方法,在开头有这么一个调用
继续进去
就能看到这么一句话了
不支持非public修饰的方法进行事务管理。
(2)多线程调用
跟上面一样的的操作,我们能够逐层进入到TransactionAspectSupport.prepareTransactionInfo
方法。
注意看以下这段话
从这里我们得知,事务信息是跟线程绑定的。
因此在多线程环境下,事务的信息都是独立的,将会导致Spring在接管事务上出现差异。
这个场景我们要尤其注意!
给大家举个例子
主线程A调用线程B保存Id为1的数据,然后主线程A等待线程B执行完成再通过线程A查询id为1的数据。
这时你会发现在主线程A中无法查询到id为1的数据。因为这两个线程在不同的Spring事务中,本质上会导致它们在Mysql中存在不同的事务中。
Mysql中通过MVCC保证了线程在快照读时只读取小于当前事务号的数据,在线程B显然事务号是大于线程A的,因此查询不到数据。
(3)数据库本身不支持事务
比如Mysql的Myisam存储引擎是不支持事务的,只有innodb存储引擎才支持。
这个问题出现的概率极其小,因为Mysql5之后默认情况下是使用innodb存储引擎了。
但如果配置错误或者是历史项目,发现事务怎么配都不生效的时候,记得看看存储引擎本身是否支持事务。
(4)未开启事务
这个也是一个比较麻瓜的问题,在Springboot项目中已经不存在了,已经有DataSourceTransactionManagerAutoConfiguration默认开启了事务管理。
但是在MVC项目中还需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。
三、失效场景集三:错误使用@Transactional
注意啦注意啦,下面这几种都是高频会出现的Bug!
(1)错误的传播机制
Spring支持了7种传播机制,分别为:
上面不支持事务的传播机制为:PROPAGATION_SUPPORTS,PROPAGATION_NOT_SUPPORTED,PROPAGATION_NEVER。
如果配置了这三种传播方式的话,在发生异常的时候,事务是不会回滚的。
(2)rollbackFor属性设置错误
默认情况下事务仅回滚运行时异常和Error,不回滚受检异常(例如IOException)。
因此如果方法中抛出了IO异常,默认情况下事务也会回滚失败。
我们可以通过指定@Transactional(rollbackFor = Exception.class)
的方式进行全异常捕获。
(3)异常被内部catch
UserService
UserService1
如上代码UserService调用了UserService1中的方法,并且捕获了UserService1中抛出的异常。
你将能看到控制台出现这样一个报错:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
默认情况下标注了@Transactional
注解的方法的事务传播机制是REQUIRED,它的特性是支持当前事务,也就说加入当前事务。我们在UserService中开始事务,然后再UserService1中抛出异常回滚UserService中的事务,将其标记为只读。
但是在UserSevice中我们捕获了异常,此时UserService上的事务认为正常提交事务。最后在提交时发现事务只读,已经被回滚,则抛出了上述异常。
因此这里如果需要对特定的异常进行捕获处理,记得再次将异常抛出,让最外层的事务感知到。
(4)嵌套事务
上面是我想同时回滚UserService与UserService1。但是也会有这种场景只想回滚UserService1中报错的数据库操作,不影响主逻辑UserService中的数据落库。
有两种方式可以实现上述逻辑:
1.直接在UserService1内的整个方法用try/catch包住
2.在UserService1使用Propagation.REQUIRES_NEW传播机制
四、总结
本文为大家分析@Transactional
注解使用过程中失效的12种场景
最后,@Transactional
注解虽香,但是复杂业务逻辑下,为了更好的管理事务与把控业务处理时事务的细粒度,我还是推荐大家使用编程式事务。
来源:https://blog.csdn.net/superjava_/article/details/122261559


猜你喜欢
- locale是通过系统设置的地区和latin输入法语言通过merger出来的,所以在系统地区设置和输入法语言中同时支持才可以在“输入语言设置
- 域对象共享数据使用ServletAPI向request域对象共享数据@RequestMapping("/testServletAP
- 前言在实际开发中,大多数情况下都需要对 SQL 传入参数以获得想要的结果集,传入的情况分为两种情况:1、SQL语句的拼接,比如表名、like
- Zookeeper和Eureka哪个更好?1、CAP理论一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求C:数据一致性
- response.setHeader设置下载文件名无效response.setContentType("application/o
- 一、准备工作1、pom依赖在pom.xml中加入POI的依赖<dependency> <groupId>org.ap
- 使用ApkTool反编译Apk下载 apktool1.4.3.tar.bz2 、apktool-install-linux-r0
- 流程图 * vs过滤器 * 是SpringMVC的技术过滤器的Servlet的技术先过过滤器,过滤器过完才到DispatcherServle
- 为了引入这个概率 首先从需求说起 即:现有某Activity专门用于手机属性设置 那么应该如何做呢? 根据已学知识 很快一个念头闪过 即:A
- 一、类加载流程类加载的流程可以简单分为三步:加载连接初始化而其中的连接又可以细分为三步:验证准备解析下面会分别对各个流程进行介绍。1.1 类
- 一、默认异常处理机制默认情况下,SpringBoot 提供 /error 请求,来处理所有异常的。1.浏览器客户端,请求头里的属性是Acce
- 算法介绍概念 TF-IDF(term frequency–inverse document
- 1、Spring的事务管理主要包括3个接口TransactionDefinition:封装事务的隔离级别,超时时间,是否为只读事务和事务的传
- reduce()简介Reduce 原意:减少,缩小根据指定的计算模型将Stream中的值计算得到一个最终结果解释:reduce 操
- Http定义了与服务器交互的不同方法,最基本的方法有4种,分别是GET,POST,PUT,DELETE。URL全称是资源描述符,我们可以这样
- springBoot是java开发中会经常用到的框架,那么在实际项目中项目配置了springBoot框架,应该如何在项目中读取配置文件中的参
- Java以命令模式设计模式1、简单介绍意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。主要解决:在软件系统中,行为
- 目录前言准备工作Nacos安装及使用入门准备三个SpringBoot服务,引入Nacos及Kafka业务解读Nacos配置创建配置读取配置监
- 一、对象与内存控制的知识点1.java变量的初始化过程,包括局部变量,成员变量(实例变量和类变量)。2.继承关系中,当使用的对象引用变量编译
- 私有构造函数私有构造函数是一种特殊的实例构造函数。它通常用在只包含静态成员的类中。如果类具有一个或多个私有构造函数而没有公共构造函数,则其他