记一次spring嵌套事务的异常
异常原因
执行嵌套事务时,由于嵌套的事务方法出错,在上层方法捕获了抛出的异常,spring依旧抛出了一个异常。
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
异常场景重现
接口
public interface DemoService {
void save() throws Exception;
void update() throws Exception;
void update2() throws Exception;
}
实现
@Component
public class DemoServiceImpl implements DemoService {
@Resource
private DataSource dataSource;
@Override
@Transactional(rollbackFor = Exception.class)
public void save() throws Exception {
System.out.println("save 执行前---");
executeSql("insert into article(title,author,content) values('test','test','test')");
((DemoService) AopContext.currentProxy()).update();
System.out.println("save 执行后---");
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void update() throws Exception {
System.out.println("update 执行前---");
try {
executeSql("insert into article(title,author,content) values('test1','test1','test1')");
((DemoService) AopContext.currentProxy()).update2();
}catch (Exception e){
System.out.println("update2 执行报错---");
}
System.out.println("update 执行后---");
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update2() throws Exception {
System.out.println("update2 执行前---");
executeSql("insert into article(title,author,content) values('test2','test2','test2')");
throw new Exception();
}
private void executeSql(String sql) {
Connection connection = DataSourceUtils.getConnection(dataSource);
try {
connection.createStatement().executeUpdate(sql);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
((DemoService) AopContext.currentProxy()).update();
这行代码是干什么的?相信大家都知道spring的事务是通过动态代理来实现的,了解动态代理的应该都知道,代理方法中直接调用内部其他方法是不会通过增强的,如果这里直接调用update()
方法,则事务增强就不会起作用。如果想要调用包含事务的方法有几种方式:
- 将当前类作为属性注册到当前类中,这样就可以通过
demoService.update()
来调用以获取增强。
@Resource
private DemoService demoServie;
- 还有一中就是文中的方法,利用spring提供的工具类
AopContext
,通过它的currentProxy()
方法获取当前代理对象,但是注意的是要先在xml文件中配置开启才能获取的到。
<aop:aspectj-autoproxy expose-proxy="true"/>
它的具体实现在代理类的方法中,底层实现实际上就是在调用方法之前将当前代理对象设置到线程变量中。
CGLIB:CglibAopProxy
中的 DynamicAdvisedInterceptor
内部类的intercept()
方法
JDK:JDKDynamicAopProxy
的invoke()
方法
if (this.advised.exposeProxy) {
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
配置文件
编辑application-transaction.xml
,properties数据库连接信息就不贴了。
<context:property-placeholder location="jdbc.properties"/>
<context:component-scan base-package="com.gaussic.transaction"/>
<aop:aspectj-autoproxy expose-proxy="true"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
测试方法
@Test
public void test() throws Exception {
// 启动一个 ApplicationContext
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:application-transaction.xml");
DemoService demoService = context.getBean(DemoService.class);
demoService.save();
}
启动上面的测试类,我们发现数据库里没有新增任何数据,按代码上面的事务注解,我们知道
-
save()
方法一开始创建了一个事务 -
update()
使用Propagation.REQUIRES_NEW
属性,所以新开了一条事务 -
update2()
方法没有配置事务的传播机制,所以沿用了update()
方法的事务(后面我会说明为什么是沿用了update
而不是save
的)
按照我们最理想的想法,update2()
方法抛错,update()
方法中捕获了异常,数据库中应该是插入save()
和update()
中新增的两条记录,有的人会说了update()
和update2()
为同一事务,应该是一起回滚了。我觉得也有道理,那就暂时理想状态就只插入save()方法中的一条数据吧。然后现实情况就是一条数据都没有插入,反而抛出了一个非业务异常。
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:724)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:485)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:291)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
...
进入异常AbstractPlatformTransactionManage
类的抛出该错误的行。发现它的方法是commit()
方法,看到commit
我就想到了事务提交。
@Override
public final void commit(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
processRollback(defStatus);
return;
}
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
processRollback(defStatus);
//抛出错误
if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
return;
}
processCommit(defStatus);
}
spring事务是使用JDK动态代理的,我们从事务代理的源头类TransactionInterceptor
开始分析。这个类是执行事务的代理类,怎么执行到这个类那就是很长一段故事了,有兴趣的可以自己从<tx:annotation-driven>
标签去慢慢解析,查看它的invoke()
方法。
@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
@Override
public Object proceedWithInvocation() throws Throwable {
return invocation.proceed();
}
});
}
跟踪invokeWithinTransaction()
方法
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
// 声明式事务
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// 开启事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal;
try {
// 执行被代理的方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
//异常回滚
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
// 提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}else{
//编程式事务,我们不分析
...
}
}
我们看到这里有提交事务的,继续跟踪其调用的commitTransactionAfterReturning()
方法
protected void commitTransactionAfterReturning(TransactionInfo txInfo) {
if (txInfo != null && txInfo.hasTransaction()) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
}
// 提交
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}
这里,我们终于看到了它调用刚刚我们查看到抛错的commit()
方法了,当然光看我们是看不出什么东西来的,我们来打几个断点来调试一下。
断点调试
我们在最开时候调用的invokeWithinTransaction()
方法中打几个断点。

开启调试,执行sava
方法到第一个断点

继续,还是停留在这个断点,这次执行了update
方法

继续,依旧是这个断点,这次执行了update2
方法,注意看newTransaction
属性,此时它的的值为false
,为false
就表示这不是一个新事物。再看它的connectionHolder
属性的地址值,将该值与前面两个方法事务中的该值比较可以发现它与update
方法中的该值相同,这就说明它的事务是沿用了update
方法创建的事务。当然,详细的设置事务信息请查看创建事务的方法createTransactionIfNecessary
。

到这里我们可以看到,三次执行的方法是按照我们代码的顺序执行的原始方法 save
-> update
-> update2
。
其中我们关注一下update
方法的事务信息中连接属性的rollbackOnly
为false
,因为该属性默认都为false
。按照来的顺序,接下来返回结果就应该是从update2
-> update
-> save
一层层的返回结果。


上面两张图是由于多次调试的原因,有些地址值不同,但不影响我们的判断。果然,update2
方法报错返回了。
注意一个细节,该事务状态中的连接属性的rollbackOnly
属性值被改成了true
。那是因为事务中一旦抛错(默认只处理RuntimeException
和Error
类型异常),但该报错的方法不是顶层事务方法(即newTransaction=false
),就会将该值改为true,再交由顶层事务方法去处理,有兴趣的请查看回滚方法completeTransactionAfterThrowing
中的实现,再接着执行

由于我们在update
方法中捕获了update2
方法抛出的异常,所以update
方法是没有再出现异常的,走到commitTransactionAfterReturning
方法中。注意,我们事务状态中连接属性的rollbackOnly
属性已经变成了true
。
此时我们已经走到我们最开始的commit
方法中
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly())
我们来观察这段代码,shouldCommitOnGlobalRollbackOnly()
方法默认false
。但注意,如果是JTA事务管理器的话,该值默认为true
,我们暂不分析。
我们来看一下defStatus.isGlobalRollbackOnly()
的底层代码
return getConnectionHolder().isRollbackOnly();
实际上就是判断连接属性中的rollbackOnly
属性,此时由于update2
方法的抛错,已经将该值该为了true
,所有我们才能够进入该方法中,我们继续。

此时我们的update
方法已经属于顶层事务方法了,从哪里看出来是顶层事务方法的了?注意观察newTransaction
属性,如果为true
,代表已经为当前事务的最外层事务方法了,所以这里抛出了我们看到的那个异常。
继续往下执行。

这个异常的抛出就导致我们外层的事务受该异常的影响也同样进行了回滚操作。最终的结果就是一条记录也没有插入数据库。
如果要想内部事务抛错不影响外层事务执行的话,就将调用内部的方法用try...catch包裹起来即可。
@Override
@Transactional(rollbackFor = Exception.class)
public void save() throws Exception {
System.out.println("save 执行前---");
executeSql("insert into article(title,author,content) values('test','test','test')");
try {
((DemoService) AopContext.currentProxy()).update();
}catch (Exception e){
System.out.println("update 执行报错---");
}
System.out.println("save 执行后---");
}
结束语
到这里,碰到的这个问题就已经全部分析完了。当碰到一次不懂的异常时,需要从源码慢慢分析理清它的实现逻辑,最重要的是知道它的入口在哪。源码的阅读是很重要的,特别是你在使用它的时候,当然这次分析撇去了很多代码只从事务代理类开始来分析,有的人会看的很懵,那是因为你不知道它是如何调用到该代理类的,这可能要你从头慢慢去看去了解。