美文网首页
记一次spring嵌套事务的异常

记一次spring嵌套事务的异常

作者: 叶子丶恬 | 来源:发表于2019-08-27 18:02 被阅读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()方法,则事务增强就不会起作用。如果想要调用包含事务的方法有几种方式:

  • 将当前类作为属性注册到当前类中,这样就可以通过demoService.update()来调用以获取增强。
  @Resource
  private DemoService demoServie;
  • 还有一中就是文中的方法,利用spring提供的工具类AopContext,通过它的currentProxy()方法获取当前代理对象,但是注意的是要先在xml文件中配置开启才能获取的到。
  <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();
    }

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

  • 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()方法中打几个断点。

断点.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 执行后---");
    }

结束语

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

相关文章

  • 记一次spring嵌套事务的异常

    异常原因 执行嵌套事务时,由于嵌套的事务方法出错,在上层方法捕获了抛出的异常,spring依旧抛出了一个异常。 异...

  • 面前温习

    Spring事务传播特性的浅析——事务方法嵌套调用的迷茫 解惑 spring 嵌套事务

  • Spring事务传播性和隔离机制

    1.Spring事务传播性 在事务出现嵌套的时候,嵌套的事务是各自独立commit,还是内层的事务合并到外层的事务...

  • Spring事务的传播属性和事务隔离级别

    事务的嵌套概念 所谓事务的嵌套就是两个事务方法之间相互调用。spring事务开启 ,或者是基于接口的或者是基于类的...

  • 事务的传播机制

    事务的嵌套概念 所谓事务的嵌套就是两个事务方法之间相互调用。spring事务开启 ,或者是基于接口的或者是基于类的...

  • 事务的传播机制和回滚策略(暂时没有整理完)

    事务的嵌套概念 所谓事务的嵌套就是两个事务方法之间相互调用。spring事务开启 ,或者是基于接口的或者是基于类的...

  • Spring嵌套事务原理

    Spring 采用保存点(Savepoint)实现嵌套事务原理Spring采用一个物理事务,但是结合着savepo...

  • Spring学习笔记(五)-事务的管理

    1.Spring的事务提交回滚 (1).运行期抛出的异常,spring会将事务回滚也就是(uncheck的异常) ...

  • Transaction rolled back because

    错误:嵌套事务中抛出异常,并且catch了异常后报错:Transaction rolled back becaus...

  • Spring

    Spring整体 Spring事务 核心抽象 配置1、编程式事务和AOP配置声明式事务配置2、 没有捕获的异常才会...

网友评论

      本文标题:记一次spring嵌套事务的异常

      本文链接:https://www.haomeiwen.com/subject/amheectx.html