介绍昨天公众号粉丝问了一个问题,说我之前面试被问到@Transactional注解在哪些场景会失效,我很无语,导致面试失败。所以今天就简单的给大家分享一下@Transactional相关的知识。@Transactional注解相信大家都不陌生。是开发中非常常用的注解。它可以确保一个方法中的多个数据库操作同时成功或失败。在使用@Transactional注解的时候需要注意很多细节,否则你会发现@Transactional总是莫名其妙的失败。1.事务事务管理是系统开发中不可或缺的一部分。Spring提供了很好的事务管理机制,主要分为程序化事务和声明式事务两种。程序化事务:指在代码中手动管理事务的提交、回滚等操作,代码侵入性比较强,如下示例:try{//TODOsomethingtransactionManager.commit(status);}catch(Exceptione){transactionManager.rollback(status);thrownewInvoiceApplyException("abnormalfailure");}声明式事务:基于面向AOP的方面,将具体业务与事务处理解耦,代码侵入性很低,所以在实际开发中,采用声明式事务更多的。声明式事务也有两种实现方式,一种是基于TX和AOPxml配置文件,另一种是基于@Transactional注解。@Transactional@GetMapping("/test")publicStringtest(){intinsert=cityInfoDictMapper.insert(cityInfoDict);}二、@Transactional简介1、@Transactional注解可以应用在什么地方?@Transactional可以应用于接口、类、类方法。作用于类:当@Transactional注解放在类上时,表示该类的所有public方法都配置了相同的事务属性信息。作用于方法:当类配置了@Transactional,方法也配置了@Transactional时,方法的事务会覆盖类的事务配置信息。作用在接口上:不推荐这种使用方式,因为一旦在Interface上标注,SpringAOP配置使用CGLib动态代理,会导致@Transactional注解失效@Transactional@RestController@RequestMappingpublicclassMybatisPlusController{@AutowiredprivateCityInfoDictMappercityInfoDictMapper;@Transactional(rollbackFor=Exception.class)@GetMapping("/test")publicStringtest()throwsException{CityInfoDictcityInfoDict=newCityInfoDict();cityInfoDict.setParentCityId(2);cityInfoDict.setCityName("2");cityInfoDict.setCityLevel("2");cityInfoDict.setCityCode("2");intinsert=cityInfoDictMapper.insert(cityInfoDict);returninsert+"";}}2、@Transactionalnote有哪些属性?传播属性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:事务隔离级别,默认值为Isolation.DEFAULT。Isolation.DEFAULT:使用底层数据库的默认隔离级别。Isolation.READ_UNCOMMITTEDIsolation.READ_COMMITTEDIsolation.REPEATABLE_READIsolation.SERIALIZABLEtimeout属性timeout:事务超时时间,默认值为-1。如果超过时间限制但事务还没有完成,则事务自动回滚。readOnly属性readOnly:指定事务是否为只读事务,默认值为false;为了忽略那些不需要事务的方法,比如读取数据,可以将read-only设置为true。rollbackFor属性rollbackFor:用于指定可以触发事务回滚的异常类型,可以指定多个异常类型。noRollbackFor属性**noRollbackFor:抛出指定的异常类型,不回滚事务,也可以指定多种异常类型。2.@Transactional注解失效场景下面结合具体代码分析@Transactional注解在哪些场景下会失效。1、@Transactional应用于非public修饰的方法。如果Transactional注解应用于非public修饰的方法,Transactional将无效。之所以在这里插入图片描述会失败,是因为在SpringAOP代理中,如上图,TransactionInterceptor(事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor的拦截方法(内部CglibAopProxy的类)或者JdkDynamicAopProxy的invoke方法会间接调用AbstractFallbackTransactionAttributeSource的computeTransactionAttribute方法获取Transactional注解的事务配置信息。protectedTransactionAttributecomputeTransactionAttribute(Methodmethod,Class>targetClass){//Don’tallowno-publicmethodsasrequired.if(allowPublicMethodsOnly()&&!Modifier.isPublic(method.getModifiers())){returnnull;}这个方法会检查目标的modifiermethod是否是public,如果不是public,则不会获取到@Transactional的属性配置信息。注意:@Transactional注释用于受保护和私有修饰的方法。虽然交易无效,但不会报错。这是我们容易出错的一点。2、@Transactional注解属性传播设置错误这种失败是由于配置错误。如果以下三个传播配置不正确,事务将不会回滚。TransactionDefinition.PROPAGATION_SUPPORTS:如果当前有事务,加入事务;如果没有当前事务,则继续以非事务方式运行。TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务模式运行,如果有当前事务,则暂停当前事务。TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务则抛出异常。3、@Transactional注解属性rollbackFor设置错误rollbackFor可以指定可以触发事务回滚的异常类型。默认情况下,Spring会抛出uncheckedexception(继承自RuntimeException的异常)或Error来回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但希望Spring回滚事务,则需要指定rollbackFor属性。在这里插入图片描述//希望自定义异常可以回滚@Transactional(propagation=Propagation.REQUIRED,rollbackFor=MyException.class如果目标方法中抛出的异常是rollbackFor指定的异常的子类,事务就是同样会回滚,Spring源码如下:privateintgetDepth(Class>exceptionClass,intdepth){if(exceptionClass.getName().contains(this.exceptionName)){//Foundit!returndepth;}//Ifwe'vegoneasfaraswecangoandhaven'tfoundit...if(exceptionClass==Throwable.class){return-1;}returngetDepth(exceptionClass.getSuperclass(),depth+1);}4.同一个类中的方法调用导致@Transactional失败同一个类中的方法调用,比如有一个类Test,它的一个方法A,A调用了这个类的方法B(不管方法B修饰的是public还是private),但是方法A没有声明一个注解事务,而B有一个方法,外部调用方法A后,方法B的事务将不起作用。这也是容易出错的地方。那么为什么会这样呢?其实这是使用了SpringAOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。//@Transactional@GetMapping("/test")privateIntegerA()throwsException{CityInfoDictcityInfoDict=newCityInfoDict();cityInfoDict.setCityName("2");/***B插入字段为3的数据*/this.insertB();/***A插入字段为2的数据*/intinsert=cityInfoDictMapper.insert(cityInfoDict);returninsert;}@Transactional()publicIntegerinsertB()throwsException{CityInfoDictcityInfoDict=newCityInfoDict();cityInfoDict.setCityName("3");cityInfoDict.setParentCityId(3);returncityInfoDictMapper.insert(cityInfoDict);}5.异常被你的catch“吃掉”,导致@Transactional失败。这是最常见的@Transactional注解失败场景@TransactionalprivateIntegerA()throwsException{ininsert=0;try{CityInfoDictcityInfoDict=newCityInfoDict();cityInfoDict.setCityName("2");cityInfoDict.setParentCityId(2);/***Ainserts字段为2*/insert=cityInfoDictMapper.insert(cityInfoDict)的数据;/***B插入字段为3*的数据/b.insertB();}catch(Exceptione){e.printStackTrace();}}如果B方法内部抛出异常,A方法尝试此时catchB方法异常,这个事务能正常回滚吗?答:不!会抛出异常:org.springframework.transaction.UnexpectedRollbackException:Transactionrolledbackbecauseithasbeenmarkedasrollback-only因为在ServiceB中抛出异常时,ServiceB识别出当前事务需要回滚,而在ServiceA中,因为你手动捕获异常并处理,ServiceA认为当前事务应该正常提交。这时候就出现了不一致,也正因为如此,才会抛出之前的UnexpectedRollbackException。spring事务在调用业务方法之前启动,commit或rollback在业务方法执行之后执行。事务是否执行取决于是否抛出运行时异常。如果抛出运行时异常但未在您的业务方法中捕获,则事务将回滚。在业务方法中,一般不需要捕获异常。如果一定要catch,必须throwthrownewRuntimeException(),或者在注解中指定异常类型@Transactional(rollbackFor=Exception.class),否则会导致事务失败,数据commit会导致datais不一致,所以有时trycatch是多余的。6、数据库引擎不支持事务的概率不高。事务能否生效或者数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换到不支持事务的myisam,事务就根本失效了。综上所述,@Transactional注解看似简单易用,但如果对它的用法稍有了解,还是会踩到很多坑。
