UnexpectedRollbackException:事务回滚,因为它已被标记为仅回滚

Posted

技术标签:

【中文标题】UnexpectedRollbackException:事务回滚,因为它已被标记为仅回滚【英文标题】:UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only 【发布时间】:2013-10-21 09:48:08 【问题描述】:

我有这种情况:

    IncomingMessage 表中获取(读取和删除)一条记录 读取记录内容 在一些表中插入一些东西 如果在步骤 1-3 中发生错误(任何异常),请在 OutgoingMessage 表中插入错误记录 否则,在 OutgoingMessage 表中插入一条成功记录

所以步骤 1,2,3,4 应该在一个事务中,或者步骤 1,2,3,5

我的流程从这里开始(这是一个计划任务):

public class ReceiveMessagesJob implements ScheduledJob 
// ...
    @Override
    public void run() 
        try 
            processMessageMediator.processNextRegistrationMessage();
         catch (Exception e) 
            e.printStackTrace();
        
    
// ...

我在ProcessMessageMediator中的主要功能(processNextRegistrationMessage):

public class ProcessMessageMediatorImpl implements ProcessMessageMediator 
// ...
    @Override
    @Transactional
    public void processNextRegistrationMessage() throws ProcessIncomingMessageException 
        String refrenceId = null;
        MessageTypeEnum registrationMessageType = MessageTypeEnum.REGISTRATION;
        try 
            String messageContent = incomingMessageService.fetchNextMessageContent(registrationMessageType);
            if (messageContent == null) 
                return;
            
            IncomingXmlModel incomingXmlModel = incomingXmlDeserializer.fromXml(messageContent);
            refrenceId = incomingXmlModel.getRefrenceId();
            if (!StringUtil.hasText(refrenceId)) 
                throw new ProcessIncomingMessageException(
                        "Can not proceed processing incoming-message. refrence-code field is null.");
            
            sqlCommandHandlerService.persist(incomingXmlModel);
         catch (Exception e) 
            if (e instanceof ProcessIncomingMessageException) 
                throw (ProcessIncomingMessageException) e;
            
            e.printStackTrace();
            // send error outgoing-message
            OutgoingXmlModel outgoingXmlModel = new OutgoingXmlModel(refrenceId,
                    ProcessResultStateEnum.FAILED.getCode(), e.getMessage());
            saveOutgoingMessage(outgoingXmlModel, registrationMessageType);
            return;
        
        // send success outgoing-message
        OutgoingXmlModel outgoingXmlModel = new OutgoingXmlModel(refrenceId, ProcessResultStateEnum.SUCCEED.getCode());
        saveOutgoingMessage(outgoingXmlModel, registrationMessageType);
    

    private void saveOutgoingMessage(OutgoingXmlModel outgoingXmlModel, MessageTypeEnum messageType)
            throws ProcessIncomingMessageException 
        String xml = outgoingXmlSerializer.toXml(outgoingXmlModel, messageType);
        OutgoingMessageEntity entity = new OutgoingMessageEntity(messageType.getCode(), new Date());
        try 
            outgoingMessageService.save(entity, xml);
         catch (SaveOutgoingMessageException e) 
            throw new ProcessIncomingMessageException("Can not proceed processing incoming-message.", e);
        
    
// ...

正如我所说,如果在步骤 1-3 中发生任何异常,我想插入一条错误记录:

catch (Exception e) 
    if (e instanceof ProcessIncomingMessageException) 
        throw (ProcessIncomingMessageException) e;
    
    e.printStackTrace();
    //send error outgoing-message
    OutgoingXmlModel outgoingXmlModel = new OutgoingXmlModel(refrenceId,ProcessResultStateEnum.FAILED.getCode(), e.getMessage());
    saveOutgoingMessage(outgoingXmlModel, registrationMessageType);
    return;

是 SqlCommandHandlerServiceImpl.persist() 方法:

public class SqlCommandHandlerServiceImpl implements SqlCommandHandlerService 
// ...
    @Override
    @Transactional
    public void persist(IncomingXmlModel incomingXmlModel) 
        Collections.sort(incomingXmlModel.getTables());
        List<ParametricQuery> queries = generateSqlQueries(incomingXmlModel.getTables());
        for (ParametricQuery query : queries) 
            queryExecuter.executeQuery(query);
        
    
// ...

但是当 sqlCommandHandlerService.persist() 抛出异常(这里是 org.hibernate.exception.ConstraintViolationException 异常),在 OutgoingMessage 表中插入一条错误记录后,当要提交事务时,我得到 UnexpectedRollbackException。 我不知道我的问题出在哪里:

Exception in thread "null#0" org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:717)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:394)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:120)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:622)
    at ir.tamin.branch.insuranceregistration.services.schedular.ReceiveMessagesJob$$EnhancerByCGLIB$$63524c6b.run(<generated>)
    at ir.asta.wise.core.util.timer.JobScheduler$ScheduledJobThread.run(JobScheduler.java:132)

我使用的是 hibernate-4.1.0-Final,我的数据库是 oracle,这是我的事务管理器 bean:

<bean id="transactionManager"
    class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

<tx:annotation-driven transaction-manager="transactionManager"
    proxy-target-class="true" />

提前致谢。

【问题讨论】:

粘贴完整的stackTrace 我注意到,当存储库本身无法完成工作时抛出此异常,而不是代码的其他部分。 【参考方案1】:

这是正常行为,原因是您的sqlCommandHandlerService.persist 方法在执行时需要一个TX(因为它标有@Transactional 注释)。但是当在processNextRegistrationMessage 内部调用它时,因为有可用的 TX,容器不会创建新的,而是使用现有的 TX。所以如果sqlCommandHandlerService.persist方法中发生任何异常,它会导致TX被设置为rollBackOnly(即使你在调用者中捕获了异常并忽略它)。

要克服这个问题,您可以对事务使用传播级别。查看this,找出最适合您要求的传播方式。

更新;阅读本文!

在一位同事向我提出了一些关于类似情况的问题后,我觉得这需要澄清一下。 尽管传播解决了这些问题,但您应该非常小心使用它们,除非您绝对了解它们的含义和工作原理,否则不要使用它们。您最终可能会保留一些数据并回滚其他一些您不希望它们以这种方式工作的数据,并且可能会出现严重错误。


编辑 Link to current version of the documentation

【讨论】:

谢谢伊恩。我不确定使用 REQUIRES_NEW 还是 NESTED!? (我已经测试了 REQUIRES_NEW 并且它有效) 这两者之间的区别在于,NESTED 就像一个子 TX,所以如果父级被提交,它将被提交,否则不会。 REQUIRES_NEW 创建一个新的 TX 并暂停现有的。所以它是单独提交的。请注意您的容器,因为并非所有容器都支持这些容器。上面的链接对支持这些类型传播的环境进行了一些解释。 如果使用 spring-test 并且 defaultRolback 为 false(@TransactionConfiguration(defaultRollback = false)),你也可能有这个异常。要解决它只需要设置 defaultRollback = true 建议在@Transactional注解中添加这个Propagation配置的方法是什么? SqlCommandHandlerServiceImpl.persist()(因此需要覆盖),还是 processNextRegistrationMessage()?单独的事务传播要求是否适用于捕获或抛出异常的事务方法? @acidnbass 两者。添加注释后,它们将具有单独的 TX。【参考方案2】:

Shyam 的回答是对的。我之前已经遇到过这个问题。这不是问题,这是 SPRING 的功能。 “事务回滚,因为它已被标记为仅回滚”是可以接受的。

结论

如果你想提交你在异常之前做了什么(本地提交),请使用 REQUIRES_NEW 如果您只想在所有进程都完成后提交(全局提交),则使用 REQUIRED 并且您只需要忽略“事务回滚,因为它已被标记为仅回滚”异常。但是您需要尝试捕获调用方 processNextRegistrationMessage() 以获得有意义的日志。

让我更详细地解释一下:

问题:我们有多少交易?答:只有一个

因为您将 PROPAGATION 配置为 PROPAGATION_REQUIRED,所以 @Transaction persist() 使用与调用者-processNextRegistrationMessage() 相同的事务。实际上,当我们遇到异常时,Spring 会为 TransactionManager 设置 rollBackOnly,因此 Spring 只会回滚一个 Transaction。

问题:但是我们在()外面有一个try-catch,为什么会发生这个异常呢? 回答因为独特的交易

    persist() 方法出现异常时

    去外面抓鱼

    Spring will set the rollBackOnly to true -> it determine we must 
    rollback the caller (processNextRegistrationMessage) also.
    

    persist() 将首先自行回滚。

    抛出 UnexpectedRollbackException 来通知我们还需要回滚调用者。 run() 中的 try-catch 将捕获 UnexpectedRollbackException 并打印堆栈跟踪

问题:为什么我们将 PROPAGATION 更改为 REQUIRES_NEW,它可以工作?

答案:因为现在 processNextRegistrationMessage() 和 persist() 在不同的事务中,所以他们只回滚他们的事务。

谢谢

【讨论】:

以上是关于UnexpectedRollbackException:事务回滚,因为它已被标记为仅回滚的主要内容,如果未能解决你的问题,请参考以下文章