目录
1)失效场景一、—— @Transactional 应用在非 public 修饰的方法上
2)失效场景二、—— @Transactional 注解属性 propagation 设置错误
3)失效场景三、—— @Transactional 注解属性 rollbackFor 设置错误
4)失效场景四、—— 同一个类中方法调用,导致@Transactional失效
5)失效场景五、—— 异常被你try…catch…了,导致@Transactional失效
一、概念
当我们调用一个基于Spring的Service接口方法(如UserService#addUser())时,它将运行于Spring管理的事务 环境中Service接口方法可能会在内部调用其它的Service接口方法以共同完成一个完整的业务操作,因此就会产生服务接口方法嵌套调用的情况, Spring通过事务传播行为控制当前的事务如何传播到被嵌套调用的目标服务接口方法中。
事务传播是Spring进行事务管理的重要概念,其重要性怎么强调都不为过。但是事务传播行为也是被误解最多的地方,在本文里,我们将详细分析不同事务传播行为的表现形式,掌握它们之间的区别。
二、事务传播行为种类
Spring在TransactionDefinition接口中规定了7种类型的事务传播行为,它们规定了事务方法和事务方法发生嵌套调用时事务如何进行传播:当前指的是当前方法的事务,即当前方法上面的注解的作用当前方法
:比如 第一个方法checkResult里面调用方法purchase:
checkResult方法用PROPAGATION_REQUIRED,purchase方法用PROPAGATION_REQUIRED,那么在checkResult方法事务中传播给purchase方法时候,purchase方法加入checkResult方法的事务,自己不开新事务。也就是当checkResult回滚的时候,那么purchase中的方法都会回滚。
事务传播行为类型 | 说明 |
PROPAGATION_REQUIRED | 如果当前方法没有事务,新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
当使用PROPAGATION_NESTED时,底层的数据源必须基于JDBC 3.0,并且实现者需要支持保存点事务机制。
三、几种容易引起误解的组合事务传播行为
当服务接口方法分别使用表1中不同的事务传播行为,且这些接口方法又发生相互调用的情况下,大部分组合都是一目了然,容易理解的。但是,也存在一些容易引起误解的组合事务传播方式。
下面,我们通过两个具体的服务接口的组合调用行为来破解这一难点。这两个服务接口分别是UserService和ForumService, UserSerice有一个addCredits()方法,ForumSerivce#addTopic()方法调用了 UserSerice#addCredits()方法,发生关联性服务方法的调用:
public class ForumService {
private UserService userService;
public void addTopic(){①调用其它服务接口的方法
//add Topic…
userService.addCredits();②被关联调用的业务方法
}
}
1 嵌套调用的事务方法
对Spring事务传播行为最常见的一个误解是:当服务接口方法发生嵌套调用时,被调用的服务方法只能声明为 PROPAGATION_NESTED。这种观点犯了望文生义的错误,误认为PROPAGATION_NESTED是专为方法嵌套准备的。这种误解遗害不 浅,执有这种误解的开发者错误地认为:应尽量不让Service类的业务方法发生相互的调用,Service类只能调用DAO层的DAO类,以避免产生嵌 套事务。
其实,这种顾虑是完全没有必要的,PROPAGATION_REQUIRED已经清楚地告诉我们:事务的方法会足够“聪明”地判断上下文是否已经存在一个事务中,如果已经存在,就加入到这个事务中,否则创建一个新的事务。
依照上面的例子,假设我们将ForumService#addTopic()和UserSerice#addCredits()方法的事务传播行为都设置为PROPAGATION_REQUIRED,这两个方法将运行于同一个事务中。
为了清楚地说明这点,可以将Log4J的日志设置为DEBUG级别,以观察Spring事务管理器内部的运行情况。下面将两个业务方法都设置为PROPAGATION_REQUIRED,Spring所输出的日志信息如下:
Using transaction object
[org.springframework.jdbc.datasource.DataSourceTransactionManager$DataSourceTransactionObject@e3849c]
①为ForumService#addTopic()新建一个事务
Creating new transaction with name [com.baobaotao.service.ForumService.addTopic]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [org.apache.commons.dbcp.PoolableConnection@dc41c5] for JDBC transaction
Switching JDBC Connection [org.apache.commons.dbcp.PoolableConnection@dc41c5] to manual commit
Bound value [org.springframework.jdbc.datasource.ConnectionHolder@ee1ede] for key [org.apache.commons.dbcp.BasicDataSource@4204] to thread [main]
Initializing transaction synchronization
Getting transaction for [com.baobaotao.service.ForumService.addTopic]
Retrieved value [org.springframework.jdbc.datasource.ConnectionHolder@ ee1ede] for key [org.apache.commons.dbcp.BasicDataSource@4204] bound to thread [main]
Using transaction object [org.springframework.jdbc.datasource.DataSourceTransactionManager$DataSourceTransactionObject@8b8a47]
②UserService#addCredits()简单地加入到已存在的事务中(即①处创建的事务)
Participating in existing transaction
Getting transaction for [com.baobaotao.service.UserService.addCredits]
Completing transaction for [com.baobaotao.service.UserService.addCredits]
Completing transaction for [com.baobaotao.service.ForumService.addTopic]
Triggering beforeCommit synchronization
Triggering beforeCompletion synchronization
Initiating transaction commit
③调用底层Connection#commit()方法提交事务
Committing JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@dc41c5]
Triggering afterCommit synchronization
Triggering afterCompletion synchronization
Clearing transaction synchronization
2 嵌套事务
将ForumService#addTopic()设置为PROPAGATION_REQUIRED时, UserSerice#addCredits()设置为PROPAGATION_REQUIRED、PROPAGATION_SUPPORTS、 PROPAGATION_MANDATORY时,运行的效果都是一致的(当然,如果单独调用addCredits()就另当别论了)。
当addTopic()运行在一个事务下(如设置为PROPAGATION_REQUIRED),而addCredits()设置为 PROPAGATION_NESTED时,如果底层数据源支持保存点,Spring将为内部的addCredits()方法产生的一个内嵌的事务。如果 addCredits()对应的内嵌事务执行失败,事务将回滚到addCredits()方法执行前的点,并不会将整个事务回滚。内嵌事务是内层事务的一 部分,所以只有外层事务提交时,嵌套事务才能一并提交。
嵌套事务不能够提交,它必须通过外层事务来完成提交的动作,外层事务的回滚也会造成内部事务的回滚
3 嵌套事务和新事务
PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED也是容易混淆的两个传播行为。
PROPAGATION_REQUIRES_NEW 启动一个新的、和外层事务无关的“内部”事务。该事务拥有自己的独立隔离级别和锁,不依赖于外部事务,独立地提交和回滚。当内部事务开始执行时,外部事务 将被挂起,内务事务结束时,外部事务才继续执行。
由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于:
PROPAGATION_REQUIRES_NEW 将创建一个全新的事务,它和外层事务没有任何关系,而
PROPAGATION_NESTED 将创建一个依赖于外层事务的子事务,当外层事务提交或回滚时,子事务也会连带提交和回滚。
重点讲一下requires_new和nested的区别,requires_new完全是一个新事务,nested是一个外部事务的子事务,是外部事务的一部分,如果嵌套事务发生异常回滚,则只回滚嵌套事务部分。而外部事务的提交和回滚均会提交/回滚嵌套事务。requires这是两个事务互不干扰,如果内部事务发生异常且异常抛到了外部调用方法,那么两个事务都回滚,如果内部事务提交成功,外部事务提交失败,外部事务不影响内部事务,或者外部事务提交成功,但内部事务失败的异常被外部事务catch住,则不影响外部事务
4 其它需要注意问题
以下几个问题值得注意:
1) 当业务方法被设置为PROPAGATION_MANDATORY时,它就不能被非事务的业务方法调用。
如将ForumService#addTopic ()设置为PROPAGATION_MANDATORY,如果展现层的Action直接调用addTopic()方法,将引发一个异常。
正确的情况是: addTopic()方法必须被另一个带事务的业务方法调用(如ForumService#otherMethod())。
所以 PROPAGATION_MANDATORY的方法一般都是被其它业务方法间接调用的。
2) 当业务方法被设置为PROPAGATION_NEVER时,它将不能被拥有事务的其它业务方法调用。假设UserService#addCredits ()设置为PROPAGATION_NEVER,当ForumService# addTopic()拥有一个事务时,addCredits()方法将抛出异常。所以PROPAGATION_NEVER方法一般是被直接调用的。
3)当方法被设置为PROPAGATION_NOT_SUPPORTED时,外层业务方法的事务会被挂起,当内部方法运行完成后,外层方法的事务重新运行。如果外层方法没有事务,直接运行,不需要做任何其它的事。
四、数据库隔离级别
数据库事务隔离级别:
一般的数据库,都包括以下四种隔离级别:
读未提交(Read Uncommitted)
读提交(Read Committed)
可重复读(Repeated Read)
串行化(Serializable)
oracle数据库支持READ COMMITTED 和 SERIALIZABLE这两种事务隔离级别。
默认系统事务隔离级别是READ COMMITTED,也就是读已提交
oracle默认隔离级别是:支持读已提交和串行化,默认读已提交
mysql默认隔离级别是:支持四种,默认可重复读
***************************************************************************************************
事务隔离级别:
添加事务注解
配置文件配置事务管理器和开始事务
<bean id = "jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<propery name="dataSource" ref="dataSource"/>
</bean>
配置namedParameterJdbcTemplate 该对象可以使用具名参数
<bean id = "namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref ="dataSource"/>
</bean>
配置事务管理器
<bean id ="transactionManager" class="org.springframework.jdbc.dataSource.DataSourceTransactionManager">
<property name="dataSource"ref="dataSource" />
</bean>
启用事务注解
<tx:annotation-drive transaction-manager="transactionManager"/>
@Transactional
1 使用propagation 指定事务传播行为,即当前事务方法被另一个事务方法调用额时候如何使用事务,默认是REQUIRED,
2 使用isolation指定事务的隔离级别,常用的READ_COMMITED
3默认情况Spring声明事务对所有的运行时异常进行回滚,也可以通过对应的属性进行设置,比如noRollbackFor={UserA.class}
4 使用readOnly指定事务是否只读,这样可以帮助数据库引擎优化事务,
5timeout指定强制回滚之前事务占用的时间
五 几种常见的事务失效常见
1)失效场景一、—— @Transactional 应用在非 public 修饰的方法上
之所以会失效是因为在Spring AOP 代理时,TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,Spring代理工厂在启动时,会扫描所有类和方法,并会检查目标方法的修饰符是否为 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属性。
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= Exception.class上面的注解,则是指定,拦截所有的Exception
4)失效场景四、—— 同一个类中方法调用,导致@Transactional失效
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
方式一:1 新加一个Service方法
方式二:2 在该Service类中注入自己
方式三:3 通过AopContent类 ,在该Service类中使用AopContext.currentProxy()获取代理对象,需要在类上加上注解
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
5)失效场景五、—— 异常被你try…catch…了,导致@Transactional失效
Spring容器是根据你方法的抛出的Exception来进行回滚的,如果你代码里自己try…catch 了,并没有抛出异常,当然,Spring容器不知道咯。
6)失效场景六、—— 数据库引擎不支持事务
事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。
六、结论
几个参数都要看,
propagation 事务传播行为,默认Require Isolation事务隔离级别以及数据库是否支持 timeout 默认-1,永不超时 readOnly 是否只读,默认false rollbackFor 感知什么类型异常,默认运行时异常RuntimeException及子类 rollbackForClassName noRollbackFor 排除什么异常不感知 noRollbackForClassName
结论一:对于@Transactional可以保证RuntimeException错误的回滚,如果想保证非RuntimeException错误的回滚,需要加上rollbackFor = Exception.class 参数。
结论二:try catch只是对异常是否可以被@Transactional 感知 到有影响。如果错误抛到切面可以感知到的地步,那就可以起作用。