引言
在项目中我们会经常使用@Transaction注解来进行事务控制,但是有时莫名没有这个事务控制没有起作用,在面试的时候,面试官也可能会问@Transaction注解在什么情况下会失效?今天我正好看到有一篇文章写到关于这个,所以就整理一下.
一.事务
我们知道在spring框架中要进行事务控制,我们有两种方式.一是【编程式事务控制】,二是【声明式事务控制】。
【编程式事务控制】
概念:顾名思义,就是我们可以通过编程代码的方式进行事务控制
缺点:代码侵入性强
实例:
try { //TODO something transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw new InvoiceApplyException("异常失败"); }
【声明式事务控制】
概念:基于AOP面向切面,它将具体业务与事务处理部分解耦。
优点:事务控制的代码和我们具体业务的代码解耦,代码侵入性小。所以在实际开发过程中,这种方式也是使用最多。声明式事务也有两种方式,一是基于TX和AOP的xml配置文件方式,二是基于@Transaction注解
二、@Transaction介绍
1.@Transaction注解可以使用的地方
@Transaction可以作用在接口,类,类方法中
- 作用类:该类的public方法都配置相同的事务属性信息.
- 作用类方法:当类配置了@Transaction,方法也配置@Transaction,方法的事务会覆盖类的事务配置信息
- 作用接口:不推荐这种方法,因为一旦标注在Interface上并且配置了Spring AOP使用CGLib动态代理,将会导致@Transaction注解失效
@Transactional @RestController @RequestMapping public class MybatisPlusController { @Autowired private CityInfoDictMapper cityInfoDictMapper; @Transactional(rollbackFor = Exception.class) @GetMapping("/test") public String test() throws Exception { CityInfoDict cityInfoDict = new CityInfoDict(); cityInfoDict.setParentCityId(2); cityInfoDict.setCityName("2"); cityInfoDict.setCityLevel("2"); cityInfoDict.setCityCode("2"); int insert = cityInfoDictMapper.insert(cityInfoDict); return insert + ""; } }
2.@Transaction的属性
①propagation
propagation 代表事务的传播行为.默认值Propagation.REQUIRED.
Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务.(也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法事务合并一个事务)
Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行
Propagation.MANDATORY:如果当前存在事务,则加入该事务,如果当前不存在事务,则抛出异常.
Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务.(当前A中的a方法默认Propagation.REQUIRED模式,类B中的b方法加上采用Propagation.REQUIRES_NEW模式,然后在a方法中调用b方法操作数据库,然而a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停a方法的事务)
Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务
Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常
Propagation.NESTED:和Propagation.REQUIRED效果一样.
②isolation属性
isolation:事务的隔离级别
③timeout属性
事务的超时时间,默认值为-1.如果超过该时间限制但事务还没有完成,则自动回滚.
④readOnly属性
指定事务是否为只读事务,默认为false;为了忽略哪些不需要事务的方法,比如读取数据,可以设置为true
⑤rollbackFor属性
用于指定能够触发事务回滚的异常类型,可以指定多个异常类型
⑥noRollbackFor属性
抛出指定的异常类型,不会改事务,也可以指定多个异常类型.
三、@Transaction失效的场景
①应用到非public修饰的方法上.之所以会失效是因为Spring AOP代理是,如图所示,TransactionInterceptor(事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy的内部类)的intercept方法或者JDKDynamicAopProxy的invoke方法会间接调用AbstractFallbackTransactionAttributeSource的computeTransactionAttribute方法,获取Transaction注解的事务配置信息.
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) { // Don't allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; }
此方法会检查目标方法的修饰符时候为public,不是public则不会获取@Transaction的属性配置信息.
②注解属性propagation属性错误.
如果配置propagation属性=下面三个属性值,则不会进行事务回滚.
TransactionDefinition.PROPAGATION_SUPPORTS
TransactionDefinition.PROPAGATION_NOT_SUPPORTED
TransactionDefinition.PROPAGATION_NEVER
③rollbackFor设置错误
rollbackFor可以指定能够处方事务回滚的异常类型.Spring默认抛出了未检查unchecked异常(继承自RuntimeException的异常)或者Error才回滚事务;其他异常不会处方回滚事务,如果在事务中抛出其他类型的异常,但却期望Spring能够回滚事务,就需要指定rollbackFor属性.
若在目标方法中抛出的异常时rollbackFor指定的异常的子类,事务同样会回滚.
private int getDepth(Class<?> exceptionClass, int depth) { if (exceptionClass.getName().contains(this.exceptionName)) { // Found it! return depth; } // If we've gone as far as we can go and haven't found it... if (exceptionClass == Throwable.class) { return -1; } return getDepth(exceptionClass.getSuperclass(), depth + 1); }
④同一个类中方法调用,导致@Transaction失效
比如有一个类Test,它的一个方法A,A再调用本类的方法B(无论方法B是public还是private修饰),当方法没有声明注解事务,而B方法有,则尾部调用方法A之后,方法B的事务是不会起作用的.这是因为使用Spring AOP代理造成的,因为只有当事务方法被当前类以为的代码调用时,才会有spring生成的代理对象来管理.
⑤异常被你的catch"吃了"导致@Transaction失效.
这样情况是最常见的一种@Transaction注解失效场景
@Transactional private Integer A() throws Exception { int insert = 0; try { CityInfoDict cityInfoDict = new CityInfoDict(); cityInfoDict.setCityName("2"); cityInfoDict.setParentCityId(2); /** * A 插入字段为 2的数据 */ insert = cityInfoDictMapper.insert(cityInfoDict); /** * B 插入字段为 3的数据 */ b.insertB(); } catch (Exception e) { e.printStackTrace(); } }
如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务还能正常回滚吗?
答案:不能!
会抛出异常:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
因为当ServiceB中抛出了一个异常以后,ServiceB标志当前事务需要rollback.当时ServiceA中由于你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit.此时就会出现了前后不一致,也就是这样,抛出了前片UnexceptedRollbackException异常,
Spring事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit或者rollback,是否是有执行取决于是否抛出runTime异常.如果抛出runTimeException并在你的业务方法中没有catch到的话,事务会回滚.
在业务方法中一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException()
,或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class)
,否则会导致事务失效,数据commit造成数据不一致,所以有些时候try catch反倒会画蛇添足。
⑥数据库引擎不支持事务
这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb
引擎。一旦数据库引擎切换成不支持事务的myisam
,那事务就从根本上失效了。
以上取自【marcozheng】公众号,https://mp.weixin.qq.com/s/enKOM3F_Xxg123HPMCFUPw,并加以整理.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架