Spring事务捕获异常后依旧回滚的解决

目录
  • 前沿
  • 问题阐述
  • 知识点前置条件
  • 问题追踪
  • 总结

前沿

一段生产事故发人深省,在Spring的声明式事务中手动捕获异常,居然判定回滚了,这是什么操作?话不多说直接上代码

@Service
public class A {

    @Autowired
    private B b;

    @Autowired
    private C c;

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
    public void operate() {
        try {
            b.insertB();
            c.insertC();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

@Service
public class B {

    @Autowired
    private BM bm;

    @Transactional(propagation = Propagation.REQUIRED)
    public int insertB() {
        return bm.insert("B");
    }
}

@Service
public class C {

    @Autowired
    private CM cm;

    @Transactional(propagation = Propagation.REQUIRED)
    public int insertC() {
        return cm.insert("C");
    }
}

问题阐述

好了大家都看到上面这段代码了,在正常的情况的我们会往B表和C表中各插入一条数据,那么当代码出现异常时又会怎么样呢?

我们现在假设B插入数据成功,但是C插入数据失败了,此时异常会上抛到A,被A中operate方法的try - cache所捕获,正常来说此时数据库中B能插入一条记录,而C表插入失败,这是我们期望的情况,但事实却不是,实际情况是B表没有插入数据,C表也没有插入数据,也就是说整个操作被Spring给回滚了

注意点
如果代码稍稍变动一下,将try - cache放在insertC的代码块中,在同样的场景下,B中会成功插入一条记录

知识点前置条件

了解Spring的传播机制的可以直接跳过

我们先要搞清楚Spring中的REQUIRED的作用
REQUIRED:如果当前没有事务就创建一个新的事务,如果当前已经存在事务就加入到当前事务
也就是说当我们的传播机制同时为REQUIRED时,A、B、C三者的事务是共用一个的,只有当A的流程全部走完时才会做一次commit或者rollback操作,不会在执行B或者C的过程中进行commit和rollback

问题追踪

好,有了一定的知识储备,我们一起来看源码
我们首先找到Spring事务的代理入口TransactionInterceptor, 当我们通过调用A类中的operate方法时会调用TransactionInterceptor的invoke方法,这是整个事务的入口,我们直接看重点invoke中的invokeWithinTransaction方法

//获取事务属性类 AnnotationTransactionAttributeSource
TransactionAttributeSource tas = getTransactionAttributeSource();
//获取事务属性
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
//获取事务管理器
final TransactionManager tm = determineTransactionManager(txAttr);

PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
//获取joinpoint
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
//注解事务会走这里
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
    // Standard transaction demarcation with getTransaction and commit/rollback calls.
    //开启事务
    TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

    Object retVal;
    try {
        // This is an around advice: Invoke the next interceptor in the chain.
        // This will normally result in a target object being invoked.
        retVal = invocation.proceedWithInvocation();
    } catch (Throwable ex) {
        // target invocation exception
        //事务回滚
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    } finally {
        cleanupTransactionInfo(txInfo);
    }

    if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
        // Set rollback-only in case of Vavr failure matching our rollback rules...
        TransactionStatus status = txInfo.getTransactionStatus();
        if (status != null && txAttr != null) {
            retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
        }
    }

    //事务提交
    commitTransactionAfterReturning(txInfo);
    return retVal;
}

不重要的代码我已经省略了,好我们来看这个流程,上面这段代码很明显反应出了,当我们程序执行过程中抛出了异常时会调用到completeTransactionAfterThrowing的回滚操作,如果没有发生异常最终会调用事务提交commitTransactionAfterReturning, 我们来分析一下

正常情况是C发生异常,并且执行到了completeTransactionAfterThrowing事务回滚,但是因为不是新创建的事务,而是加入的事务所以并不会触发回滚操作,而在A中捕获了该异常,并且最终走到commitTransactionAfterReturning事务提交,事实是这样的吗?

事实上就是这样的,那就奇怪了,我明明提交了,怎么反而回滚了,我们继续往下看

public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
    // Use defaults if no transaction definition given.
    TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

    //重点看.. DataSourceTransactionObject拿到对象
    Object transaction = doGetTransaction();
    boolean debugEnabled = logger.isDebugEnabled();

    //第一次进来connectionHolder为空的, 所以不存在事务
    if (isExistingTransaction(transaction)) {
        // Existing transaction found -> check propagation behavior to find out how to behave.
        //如果不是第一次进来, 则会走这个逻辑
        return handleExistingTransaction(def, transaction, debugEnabled);
    }

    // Check definition settings for new transaction.
    if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
        throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
    }

    // No existing transaction found -> check propagation behavior to find out how to proceed.
    if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
        throw new IllegalTransactionStateException(
                "No existing transaction found for transaction marked with propagation 'mandatory'");
    }
    //第一次进来大部分会走这里(传播属性是 Required | Requested New | Nested)
    else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
            def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
            def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
        //先挂起
        SuspendedResourcesHolder suspendedResources = suspend(null);
        if (debugEnabled) {
            logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
        }
        try {
            //开启事务
            return startTransaction(def, transaction, debugEnabled, suspendedResources);
        } catch (RuntimeException | Error ex) {
            resume(null, suspendedResources);
            throw ex;
        }
    } else {
        // Create "empty" transaction: no actual transaction, but potentially synchronization.
        if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
            logger.warn("Custom isolation level specified but no actual transaction initiated; " +
                    "isolation level will effectively be ignored: " + def);
        }
        boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
        return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
    }
}

这段代码是开启事务的代码,我们来看,当我们A第一次走进来的时候,此时是没有事务的,所以isExistingTransaction方法不成立,往下走,因为我们的传播机制是REQUIRED,所以我们会走到startTransaction方法中

private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction, boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {
    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    //创建一个新的事务状态, 注意这里的newTransaction 属性为true
    DefaultTransactionStatus status = newTransactionStatus(
            definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
    //开启事务
    doBegin(transaction, definition);
    //开启事务后, 改变事务状态
    prepareSynchronization(status, definition);
    return status;
}

好这里我们只需要关注一个点那就是newTransactionStatus的第三个参数newTransaction,只有当我们新创建一个事务的时候才会为true,这个属性很重要,我们后续还会用到它

好了,到这里第一次的事务开启就已经完成了,然后我们会调用业务逻辑,当调用insertB时,又会走到getTransaction,我们继续来看它,此时isExistingTransaction就可以拿到值了,因为A已经帮我们创建好了事务,此时会调用到handleExistingTransaction方法

//如果第二次进来还是PROPAFGATION_REQUIRED, 走这里, newTransation为false
return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);

针对REQUIRED有用的代码就这一句,其他全部不用看,同样的我们看到第三个参数newTransaction,这里是false了,说明是加入了之前的事务,而不是自己新创建的,然后执行业务代码,最后走到commit,我们来看看commit中做了什么

//如果有回滚点
if (status.hasSavepoint()) {
    if (status.isDebug()) {
        logger.debug("Releasing transaction savepoint");
    }
    unexpectedRollback = status.isGlobalRollbackOnly();
    status.releaseHeldSavepoint();
}
//如果是新事务, 则提交事务
else if (status.isNewTransaction()) {
    if (status.isDebug()) {
        logger.debug("Initiating transaction commit");
    }
    unexpectedRollback = status.isGlobalRollbackOnly();
    doCommit(status);
}
else if (isFailEarlyOnGlobalRollbackOnly()) {
    unexpectedRollback = status.isGlobalRollbackOnly();
}

它什么事情都没有做,为什么?因为我们的newTransaction不为true,所以当我们的代码在operate方法全部执行完以后才会走到这里

好接下来我们来看insertC,前面的流程都一模一样,我们直接看到回滚代码

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
    try {
        boolean unexpectedRollback = unexpected;

        try {
            triggerBeforeCompletion(status);

            if (status.hasSavepoint()) {
                if (status.isDebug()) {
                    logger.debug("Rolling back transaction to savepoint");
                }
                status.rollbackToHeldSavepoint();
            } else if (status.isNewTransaction()) {
        if (status.isDebug()) {
                    logger.debug("Initiating transaction rollback");
                }
                doRollback(status);
            } else {
                // Participating in larger transaction
                if (status.hasTransaction()) {
                    if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
                        if (status.isDebug()) {
                            logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                        }
                        doSetRollbackOnly(status);
                    } else {
                        if (status.isDebug()) {
                            logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
                        }
                    }
                } else {
                    logger.debug("Should roll back transaction but cannot - no transaction available");
                }
                // Unexpected rollback only matters here if we're asked to fail early
                if (!isFailEarlyOnGlobalRollbackOnly()) {
                    unexpectedRollback = false;
                }
            }
        } catch (RuntimeException | Error ex) {
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
            throw ex;
        }

        triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

        // Raise UnexpectedRollbackException if we had a global rollback-only marker
        if (unexpectedRollback) {
            throw new UnexpectedRollbackException(
                    "Transaction rolled back because it has been marked as rollback-only");
        }
    } finally {
        cleanupAfterCompletion(status);
    }
}

我们的insertC方法同样它的newTransaction不是true,所以最终会走到doSetRollbackOnly,这个方法重中之重,最后会调用这样一段代码

public void setRollbackOnly() {
    this.rollbackOnly = true;
}

然后我们就要执行到我们的关键代码A中的operate的提交代码了

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, false);
        return;
    }

    if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
        if (defStatus.isDebug()) {
            logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
        }
        processRollback(defStatus, true);
        return;
    }

    //执行事务提交
    processCommit(defStatus);
}

好了,看到这大家都明白了吧,在commit中,Spring会去判断defStatus.isGlobalRollbackOnly有没有抛出过异常被Spring所拦截,如果有,那么就不会执行commit操作,转而执行processRollback回滚操作

总结

在Spring的REQUIRED中,只要异常被Spring捕获到过,那么Spring最终就会回滚整个事务,即使自己在业务中已经捕获
所以我们回到最初的代码,如果我们希望Spring不进行回滚,那么我们只用将try-cache方法insertC方法中就可以,因为此时抛出的异常并不会被Spring所拦截到

到此这篇关于Spring事务捕获异常后依旧回滚的解决的文章就介绍到这了,更多相关Spring事务捕获异常后回滚 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • spring事务异常回滚实例解析

    最近遇到了事务不回滚的情况,我还考虑说JPA的事务有bug?我想多了....... 为了打印清楚日志,很多方法我都加tyrcatch,在catch中打印日志.但是这边情况来了,当这个方法异常时候日志是打印了,但是加的事务却没有回滚. 例: 类似这样的方法不会回滚(一个方法出错,另一个方法不会回滚): if(userSave){ try { userDao.save(user); userCapabilityQuotaDao.save(capabilityQuota); } catch (Exce

  • Spring异常捕获且回滚事务解决方案

    默认spring只在发生未被捕获的runtimeexcetpion时才回滚. 最笨的办法:代码级控制:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 为何在aop advitor中配置rollba-for="java.lang.Exception"异常时不回滚呢? 问题已解决: 原理:spring aop 异常捕获原理:被拦截的方法需显式抛出异常,并不能经任何处理,这样aop代理才能捕获到方法

  • Spring事务管理只对出现运行期异常进行回滚

    一.结论 Spring的事务管理默认只对出现运行期异常(java.lang.RuntimeException及其子类)进行回滚. 如果一个方法抛出Exception或者Checked异常,Spring事务管理默认不进行回滚. 关于异常的分类一下详细介绍: 1.基本概念 看java的异常结构图  Throwable是所有异常的根,java.lang.Throwable Error是错误,java.lang.Error Exception是异常,java.lang.Exception 2.Excep

  • Spring事务捕获异常后依旧回滚的解决

    目录 前沿 问题阐述 知识点前置条件 问题追踪 总结 前沿 一段生产事故发人深省,在Spring的声明式事务中手动捕获异常,居然判定回滚了,这是什么操作?话不多说直接上代码 @Service public class A {     @Autowired     private B b;     @Autowired     private C c;     @Transactional(propagation = Propagation.REQUIRED, isolation = Isolat

  • 关于Spring的@Transaction导致数据库回滚全部生效问题(又删库跑路)

    1 前言 很多需要使用事务的场景,都只是在方法上直接添加个@Transactional注解 但是,你以为这真的够了吗? 事务如果未达到完美效果,在开发和测试阶段都难以被发现,因为你难以考虑到太多意外场景.但当业务数据量发展,就可能导致大量数据不一致的问题,就会造成前人栽树后人踩坑,需要大量人力排查解决问题和修复数据. 2 如何确认Spring事务生效了? 使用@Transactional一键开启声明式事务, 这就真的事务生效了?过于信任框架总有"意外惊喜".来看如下案例 领域层 实体

  • 基于Postgresql 事务的提交与回滚解析

    用过oracle或mysql的人都知道在sqlplus或mysql中,做一个dml语句,如果发现做错了,还可以rollback;掉,但在PostgreSQL的psql中,如果执行一个dml,没有先运行begin;的话,一执行完就马上提交了,不能回滚,这样容易导致误操作的发生,有没有什么办法避免这个风险呢? 当然有,在psql中默认是打开自动提交的,我们可以关闭自动提交,方法如下: 设置\set AUTOCOMMIT off test=# create table test1 (x int); C

  • Redis事务为什么不支持回滚

    目录 前言 Redis 有事务吗 Redis 事务实现原理 Redis 事务 ACID 特性 A - 原子性 C - 一致性 I - 隔离性 D - 持久性 watch 命令 watch 命令的作用 watch 原理分析 总结 前言 事务是关系型数据库的特征之一,那么作为 Nosql 的代表 Redis 中有事务吗?如果有,那么 Redis 当中的事务又是否具备关系型数据库的 ACID 四大特性呢? Redis 有事务吗 这个答案可能会令很多人感到意外,Redis 当中是存在"事务"的

  • MySQL数据库误操作后快速回滚的方法

    基本上每个跟数据库打交道的程序员(当然也可能是你同事)都会碰一个问题,MySQL误操作后如何快速回滚?比如,delete一张表,忘加限制条件,整张表没了.假如这还是线上环境核心业务数据,那这事就闹大了.误操作后,能快速回滚数据是非常重要的. binlog2sql快速回滚 首先,确认你的MySQL server开启了binlog,设置了以下参数: [mysqld] server-id = 1 log_bin = /var/log/mysql/mysql-bin.log max_binlog_siz

  • MySQL数据库误删回滚的解决

    某次一不小心,用了delete from xxx 删除了几条重要数据,在网上找了很多方法,但都比较零散,打算记录本次数据找回的过程.大致分为以下几步 1.查看binlog是否开启 # log_bin是ON,就说明打开了 OFF就是关闭状态,以下操作,只有为 ON 时有效. show variables like 'log_bin'; 2.找到binlog文件名 show master logs; 运行以上代码,如下图 TS1-bin.000009 就是我们要找的文件名 3.查看binlog日志位

  • spring在service层的方法报错事务不会回滚的解决

    目录 spring在service层方法报错事务不会回滚 解决方法 service手动回滚问题 spring在service层方法报错事务不会回滚 @Transactional(rollbackFor = {Exception.class}) public void insertData() throws Exception {     // 业务代码1     business1();          // 业务代码2     business2();          // 业务代码3  

  • mysql实现事务的提交与回滚的实例详解

    最近要对数据库的数据进行一个定时迁移,为了防止在执行过程sql语句因为某些原因报错而导致数据转移混乱,因此要对我们的脚本加以事务进行控制. 首先我们建一张tran_test表 CREATE TABLE tran_test( f1 VARCHAR(10) NOT NULL, f2 INT(1) DEFAULT NULL, PRIMARY KEY (f1) )ENGINE=INNODB CHARSET=utf8 我想对tran_test插入两条数据,但是为了防止插入中报错,因此我要把插入语句控制在一

  • mysql实现事务的提交和回滚实例

    mysql创建存储过程的官方语法为: 复制代码 代码如下: START TRANSACTION | BEGIN [WORK]COMMIT [WORK] [AND [NO] CHAIN] [[NO] RELEASE]ROLLBACK [WORK] [AND [NO] CHAIN] [[NO] RELEASE]SET AUTOCOMMIT = {0 | 1} 我这里要说明的mysql事务处理多个SQL语句的回滚情况.比如说在一个存储过程中启动一个事务,这个事务同时往三个表中插入数据,每插完一张表需要

  • 详解Java的JDBC API中事务的提交和回滚

    如果JDBC连接是在自动提交模式下,它在默认情况下,那么每个SQL语句都是在其完成时提交到数据库. 这可能是对简单的应用程序,但有三个原因,你可能想关闭自动提交和管理自己的事务: 为了提高性能 为了保持业务流程的完整性 使用分布式事务 若要控制事务,以及何时更改应用到数据库.它把单个SQL语句或一组SQL语句作为一个逻辑单元,而且如果任何语句失败,整个事务失败. 若要启用,而不是JDBC驱动程序默认使用auto-commit模式手动事务支持,使用Connection对象的的setAutoComm

随机推荐