@Transactional 相信大家都很熟悉注释。它是开发中常用的一种注释,可以保证方法中多个数据库的操作同时成功或失败。使用@transactional注释时,需要注意很多细节,否则你会发现@transactional总是莫名其妙地失败
一、事务
Spring提供了良好的事务管理机制,主要分为编程事务和声明事务。
编程事务:指代码中手动管理事务的提交、回滚等操作,具有较强的代码侵入性,如下:
try {
//TODO something
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw new InvoiceApplyException(“异常失败”);
}
声明事务:基于AOP面向截面,它解耦了特定的业务和事务处理部分,代码侵入性很低,因此在实际开发中使用了更多的声明事务。实现声明事务也有两种方式,一种是基于TX和AOP的XML配置文件,另一种是基于@Transactional注释。
@Transactional
@GetMapping("/test")
public String test() {
int insert = cityInfoDictMapper.insert(cityInfoDict);
}
二、@Transactional介绍
1、@Transactional注释能在哪里发挥作用?
@Transactional 可作用于接口、类、类的方法。
作用于类:当把@Transactional 注解放在类上时,说明所有这类public方法都配置了相同的事务属性信息。
作用于方法:当类配置@Transactional,方法也配置好了@Transactional,该方法的事务将覆盖事务配置信息。
作用于接口:不建议使用这种方法,因为一旦标记在Interface上,配置Spring AOP 使用CGLib动态代理会导致@Transactional注释失败
@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、@Transactional注释的属性是什么?
propagation属性
propagation 代表事务的传播行为默认值 Propagation.REQUIRED,其他属性信息如下:
Propagation.REQUIRED:如果目前有事务,则加入事务,如果目前没有事务,则创建新的事务。( 也就是说,如果A方法和B方法都添加注释,在默认传播模式下,A方法内部调用B方法将两种方法的事务合并为一种事务 )
Propagation.SUPPORTS:如果目前有事务,则加入事务;如果目前没有事务,则继续以非事务的形式运行。
Propagation.MANDATORY:若目前有事务,则加入事务;若目前没有事务,则抛出异常。
Propagation.REQUIRES_NEW:如果现在有事务,重新创建新的事务,暂停现在的事务。( 当类A中的 a 默认Propagation.类B中的REQUIRED模式 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_UNCOMMITTED
Isolation.READ_COMMITTED
Isolation.REPEATABLE_READ
Isolation.SERIALIZABLE
timeout 属性
timeout :事务的超时间,默认值 -1.如果超过时间限制但事务尚未完成,则自动回滚事务。
readOnly 属性
readOnly :指定的事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
rollbackFor 属性
rollbackFor :指定可触发事务回滚的异常类型,可指定多种异常类型。
norolbackfor属性**
noRollbackFor:抛出指定的异常类型,不回滚事务,也可指定多种异常类型。
三、@Transactional故障场景
接下来,我们将结合具体的代码来分析哪些场景,@Transactional 注释会失效。
1、@Transactional 应用在非 public 修饰方法
如果Transactional注释应用于非public Transactional将在修改方法上失效。
之所以会失效,是因为Spring AOP 代理时,如上图所示 TransactionInterceptor (事务AOP)在实施对象方法前后拦截,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不会获得@Transactional 属性配置信息。
注意:protected、private 使用修饰方法 @Transactional 注意,虽然事务无效,但不会有任何错误,这是我们容易犯的错误。
2、@Transactional 注解属性 propagation 设置错误
这种失败是由于配置错误造成的。如果配置错误,有三种 propagation,事务不会回滚。
TransactionDefinition.PROPAGATION_SUPPORTS:如果目前有事务,则加入事务;如果目前没有事务,则继续以非事务的形式运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务的形式运行,如果当前事务存在,则挂起当前事务。
TransactionDefinition.PROPAGATION_NEVER:以非事务的形式运行,如果目前有事务,则抛出异常。
3、@Transactional 注解属性 rollbackFor 设置错误
rollbackFor 可指定能触发事务回滚的异常类型。Spring默认抛出未检查unchecked异常(继承自 Runtimeexception异常)或 只有Error才能回滚;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但期望 Spring 如果能够回滚,则需要指定 rollbackFor属性。
1// 希望自定义的异常可以回滚
2@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
在目标方法中抛出的异常是 rollbackFor 指定的异常子类,事务也会回滚。Spring源代码如下:
1private int getDepth(Class exceptionClass, int depth) {
2 if (exceptionClass.getName().contains(this.exceptionName)) {
3 // Found it!
4 return depth;
5}
6 // If we've gone as far as we can go and haven't found it...
7 if (exceptionClass == Throwable.class) {
8 return -1;
9}
十return getDepth(exceptionClass.getSuperclass(), depth + 1);
11}
4、在同一类中调用方法会导致@Transactional失效
在开发过程中,不可避免地会调用同一类的方法。例如,有一个类Test,它的一个方法A,然后调用此类方法B(无论方法B是用public还是private进行修改),但方法A没有声明解释事务,而B方法是。在外部调用方法A之后,方法B的事务将不起作用。这也是一个经常犯错误的地方。
那为什么会发生这种情况呢?其实这还是因为Spring的使用? AOP代理是由Spring生成的代理对象管理的,因为只有当事务方法被当前类以外的代码调用时。
//@Transactional
@GetMapping("/test")
private Integer A() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
/**
* B 插入字段为 3的数据
*/
int insert=insertB();
/**
* A 插入字段为 2的数据
*/
int insert = cityInfoDictMapper.insert(cityInfoDict);
return insert;
}
@Transactional()
public Integer insertB() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("3");
cityInfoDict.setParentCityId(3);
return cityInfoDictMapper.insert(cityInfoDict);
}
5、异常被你的 catch“吃”导致@Transactional失效
这种情况是最常见的@Transactional注释失效场景
@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 catchb方法异常,这件事还能正常回滚吗?
答:不能!
会抛出异常:
因为当ServiceB抛出异常时,ServiceB识别当前事务需要rollback。但在ServiceA中,ServiceA认为目前的事务应该是正常的commit,因为你手动捕获并处理这种异常。这时就出现了前后不一致,正因为如此,前面的Unexpectedrolbackexception被抛出。
spring的事务是在调用业务方法之前开始的,业务方法完成后才执行commit or rollback,事务是否执行取决于是否抛出runtime异常。如果抛出runtimee。 exception 如果你的业务方法中没有catch,事务就会回滚。
在业务方法中,一般不需要catch异常。如果非要catch,必须抛出throww new RuntimeException()或在注释中指定异常类型的抛掷@Transactional(rollbackFor=Exception.class),否则会导致事务失效,数据commit会导致数据不一致,所以有时候try 相反,catch会画蛇添足。
6、数据库引擎不支持事务
这种情况的概率不高,数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换到不支持事务的myisam,事务将从根本上失败。
总结
@Transactional 注释看似简单易用,但如果对它的用法知之甚少,还是会踩很多坑。