记一次spring嵌套事务的异常

2019-08-27  本文已影响0人  叶子丶恬

异常原因

执行嵌套事务时,由于嵌套的事务方法出错,在上层方法捕获了抛出的异常,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()方法,则事务增强就不会起作用。如果想要调用包含事务的方法有几种方式:

  @Resource
  private DemoService demoServie;
  <aop:aspectj-autoproxy expose-proxy="true"/>

它的具体实现在代理类的方法中,底层实现实际上就是在调用方法之前将当前代理对象设置到线程变量中。

CGLIB:CglibAopProxy中的 DynamicAdvisedInterceptor内部类的intercept()方法
JDK:JDKDynamicAopProxyinvoke()方法

    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();
    }

启动上面的测试类,我们发现数据库里没有新增任何数据,按代码上面的事务注解,我们知道

按照我们最理想的想法,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()方法中打几个断点。

断点.png

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

执行save方法.png

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

执行update方法.png

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

执行update2方法.png

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

update2方法报错执行回滚前.png update2方法报错执行回滚后.png

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

updae方法返回.png

由于我们在update方法中捕获了update2方法抛出的异常,所以update方法是没有再出现异常的,走到commitTransactionAfterReturning方法中。注意,我们事务状态中连接属性的rollbackOnly属性已经变成了true
此时我们已经走到我们最开始的commit方法中

if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) 

我们来观察这段代码,shouldCommitOnGlobalRollbackOnly()方法默认false。但注意,如果是JTA事务管理器的话,该值默认为true,我们暂不分析。
我们来看一下defStatus.isGlobalRollbackOnly()的底层代码

return getConnectionHolder().isRollbackOnly();

实际上就是判断连接属性中的rollbackOnly属性,此时由于update2方法的抛错,已经将该值该为了true,所有我们才能够进入该方法中,我们继续。

update.png

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

save方法返回.png

这个异常的抛出就导致我们外层的事务受该异常的影响也同样进行了回滚操作。最终的结果就是一条记录也没有插入数据库。

如果要想内部事务抛错不影响外层事务执行的话,就将调用内部的方法用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 执行后---");
    }

结束语

到这里,碰到的这个问题就已经全部分析完了。当碰到一次不懂的异常时,需要从源码慢慢分析理清它的实现逻辑,最重要的是知道它的入口在哪。源码的阅读是很重要的,特别是你在使用它的时候,当然这次分析撇去了很多代码只从事务代理类开始来分析,有的人会看的很懵,那是因为你不知道它是如何调用到该代理类的,这可能要你从头慢慢去看去了解。

上一篇 下一篇

猜你喜欢

热点阅读