目录
一、Spring
管理事务的两种方式
在项目系统的事务管理中,Spring 为我们提供了两种管理事务的方式:编程式事务和声明式事务。下面将对这两种方式进行讲解介绍。
1、编程式事务
编程式事务:是指在代码中手动地管理事务的提交、回滚等操作,代码侵入性比较强,代码示例如下:
try {
...
// 手动提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 发生异常,手动回滚
transactionManager.rollback(status)
}
2、声明式事务
声明式事务是建立在 AOP
之上的,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务的实现方式:
- 1、基于
TX
和AOP
的xml
配置文件方式; - 2、基于
@Transactional
注解。
优点:
最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明,便可以将事务规则应用到业务逻辑中。
缺点:
和编程式事务相比,声明式事务唯一不足地方是:它的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
总结:显然声明式事务要优于编程式事务,这正是 Spring 提倡的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的
POJO
对象,只要加上注解就可以获得完全的事务支持。
二、@Transactional
注解介绍
1、@Transactional
使用方法
@Transactional
用于声明事务的开启,可以作用在接口、类、类方法上:
- 作用在类上: 当把 @Transactional 注解放在类上时,表示所有该类的
public
方法都配置相同的事务属性信息; - 作用在方法上: 当类上配置了@Transactional,方法也配置了@Transactional,则方法的事务会覆盖类的事务配置信息;
- 作用在接口: 不推荐这种使用方法,因为一旦标注在
Interface
上并且配置了Spring AOP
使用CGLib
动态代理,将会导致 @Transactional 注解失效。
2、@Transactional
属性介绍
1)propagation
属性
表示事务的传播行为,默认值为 Propagation.REQUIRED
,可选属性值如下:
Propagation.REQUIRED
:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务(也就是说如果A方法和B方法都添加了注解,并且都使用的是该传播模式,则A方法内部调用B方法时,会把两个方法的事务合并为一个事务);Propagation.REQUIRES_NEW
:重新创建一个新的事务,如果当前存在事务,暂停当前的事务(也就是说在同一个类中,A方法为 REQUIRED,B方法为 REQUIRED_NEW,A调用B的话,A的事务会被B暂停,相当于A没有事务);Propagation.NOT_SUPPORTED
:以非事务的方式运行,如果当前存在事务,暂停当前的事务;Propagation.NEVER
:以非事务的方式运行,如果当前存在事务,则抛出异常;Propagation.NESTED
:和 Propagation.REQUIRED 效果一样。
2)isolation
属性
表示事务的隔离级别,默认的属性值为 Isolation.DEFAULT
,可选属性值如下:
Isolation.DEFAULT
:使用底层数据库默认的隔离级别;Isolation.READ_UNCOMMITTED
:未提交读;Isolation.READ_COMMITTED
:已提交读;Isolation.REPEATABLE_READ
:可重复读;Isolation.SERIALIZABLE
:串型化。
3)rollbackFor
属性
用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。Spring 默认是要抛出了未检查 unchecked 异常(继承自 RuntimeException 的异常)或者 Error 才回滚事务,其他异常不会触发回滚事务。
4)noRollbackFor
属性
指定哪些异常类型被抛出时不会回滚事务,可以指定多个异常类型。
5)timeout
属性
事务的超时时间,默认值为 -1(表示没有超时时间)。如果超过该时间限制但事务还没有完成,则自动回滚事务。
6)readOnly
属性
指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据操作,这些是不需要进行回滚操作的,所以可设置 readOnly 为 true。
三、@Transactional
事务失效场景
1、数据库引擎不支持事务
以 MySQL 为例,其 MyISAM 引擎是不支持事务的,只有 InnoDB 引擎才支持事务,所以要支持事务的话需要使用 InnoDB。
注意:从 MySQL 5.5 开始的默认存储引擎是:InnoDB,之前默认的都是:MyISAM。
2、类没有交给 Spring 管理
例如以下示例:
// @Service
public class MyService {
@Transactional
public void insert() {
// do something
}
}
如果把 @Service
注解注释掉,这个类就不会被加载成一个 Bean,也就不会被 Spring 管理,事务自然就失效了。
3、修饰的方法不是 public 类型
如果 Transactional 注解应用在非 public 修饰的方法上,则 Transactional 将会失效。我们可以查看 Spring 的官方文档:
When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.
具体原理是:在 Spring AOP 代理时,TransactionInterceptor(事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept
方法或 JdkDynamicAopProxy的 invoke
方法会间接调用 AbstractFallbackTransactionAttributeSource 的 computeTransactionAttribute
方法,获取 Transactional 注解的事务配置信息:
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 注解,虽然事务无效,但不会有任何报错,如果要用在非 public 方法上,可以开启 AspectJ
代理模式。
4、自身调用问题
情况1:没有事务的方法调用有事务的方法
@Service
public class MyService {
public void A() {
B();
}
@Transactional
public void B() {
// do something
}
}
这种情况下,方法B的事务是不会生效的,因为 Spring 的事务是通过 Apring AOP
动态代理来实现的,这种情况下相当于是发生了自身调用,从而绕过了 Spring 的代理导致事务失效。默认情况下,只有在外部调用事务方法时,才会由 Spring 生成的代理对象来管理,这样事务才会生效。
情况2:一个事务被另一个事务暂停
@Service
public class MyService {
@Transactional
public void A() {
B();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void B() {
// do something
}
}
这种情况下,由于A方法使用的是默认事务传播行为 Propagation.REQUIRED,而调用了B方法后,由于B方法使用的事务传播行为 Propagation.REQUIRES_NEW,会把A的事务暂停掉,相当于没有事务。这样就和第一种情况那样:没有事务的方法调用有事务的方法,事务不会生效。
针对以上问题的解决办法是:使用代理对象来调用事务方法,比如使用 AsprctJ:
public class MyService {
@Transactional
public void A() {
// 使用动态代理调用本地其他事务
MyService myService = (MyService) AopContext.currentProxy();
myService.B();
}
@Transactional(propagation=Propagation.REQUIRED_NEW)
public void B() {
// ....
}
}
5、主动失效事务
如果注解的属性 propagation
的属性值设置为:Propagation.NOT_SUPPORTED、Propagation.NEVER,表示不以事务运行:
@Service
public class MyService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void A() {
// do something
}
@Transactional(propagation = Propagation.NEVER)
public void B() {
// do something
}
}
这些情况都表示主动放弃事务,自然不会生效事务了。
6、异常被捕获
这是最常见的事务失效的场景:
@Service
public class MyService {
@Transactional
public void A() throws Exception {
try {
// do something
} catch (Exception e) {
log.error(e);
}
}
}
当方法中出现异常之后,事务管理器就会标记当前事务需要回滚,但由于我们手动对异常进行了捕获并处理,从而导致事务管理器认为当前事务是正常提交的,此时就出现了前后不一致的情况,就会导致抛出 UnexpectedRollbackException
异常。
Spring 的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行 commit or rollback,事务是否执行取决于是否抛出 runtime 异常。如果抛出 runtime exception 并在你的业务方法中没有 catch 到的话,事务会回滚。
在业务方法中一般不需要 catch 异常,如果非要 catch 一定要抛出 throw new RuntimeException()
,或者注解中指定抛异常类型 @Transactional(rollbackFor=Exception.class)
,否则会导致事务失效。
7、异常类型错误
rollbackFor
可以指定能够触发事务回滚的异常类型。Spring 默认是要抛出了未检查 unchecked 异常(继承自 RuntimeException 的异常)或者 Error 才回滚事务,其他异常不会触发回滚事务。
但若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。更多异常相关知识可以了解我的另一篇博客:【Java基础】之深入讲解Java异常
8、数据源没有配置事务管理器
配置数据源方法如下:
@Bean
public PlatformTransactionManager prodTransactionManager(@Qualifier("prodDataSource") DataSource prodDataSource) {
return new DataSourceTransactionManager(prodDataSource);
}
只有当前数据源配置了事务管理器才会生效事务。