【问题标题】:Rollback for doubly nested transaction bypasses savepoint双重嵌套事务的回滚绕过保存点
【发布时间】:2020-02-22 03:37:09
【问题描述】:

这与标题所说的不完全一样,但接近。考虑一下这些 Spring bean:

@Bean
class BeanA {

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = EvilException.class)
    public void methodA() {
        /* ... some actions */
        if (condition) {
            throw new EvilException();
        }
    }
}

@Bean
class BeanB {
    @Autowired private BeanA beanA;
    final int MAX_TRIES = 3;

    @Transactional(propagation = Propagation.NESTED)
    public void methodB() {
        // prepare to call Bean A
        try {
            beanA.methodA();
            /* maybe do some more things */
        }
        catch (EvilException e) {
           /* recover from evil */
        }
    }
}

@Bean
class MainWorkerBean {
    @Autowired private BeanB beanB;
    @Autowired private OtherBean otherBean;

    @Transactional(propagation = Propagation.REQUIRED)
    public void doSomeWork() {
        beanB.methodB();
        otherBean.doSomeWork();
    }
}

重要提示:我正在使用支持保存点的 JDBC 事务管理器。

我期望这样做是,当EvilException 被抛出时,BeanA 的事务被回滚,在这个设置中,它恰好是通过启动methodB 创建的保存点。但是,情况似乎并非如此。

在使用调试工具时,我看到的是:

  1. doSomeWorkMainWorkerBean 启动时,会创建新事务
  2. methodB 启动时,事务管理器正确初始化保存点并将其交给TransactionInterceptor
  3. methodA 启动时,事务管理器再次看到Propagation.REQUIRED,并再次发出对实际 JDBC 事务的干净引用,它不知道保存点

这意味着当抛出异常时,TransactionStatus::hasSavepoint返回false,这会导致整个全局事务回滚,所以恢复和进一步的步骤就像丢失一样,但我的实际代码不知道回滚(因为我已经为它写了恢复)。

目前,我不能考虑将BeanA 的事务更改为Propagation.NESTED。诚然,看起来它将允许我进行更多本地回滚,但它会过于本地化,因为据我了解,Spring 将有两个保存点,并且只回滚 BeanA 保存点,而不是 @987654336 @ 一,我愿意。

我是否还缺少其他任何东西,例如配置选项,它会使与 Propagation.REQUIRED 的内部事务认为它在保存点内运行,并回滚到保存点,而不是全部?

现在我们使用的是 Spring 4.3.24,但我已经爬过了他们的代码,无法发现任何相关的变化,所以我认为升级对我没有帮助。

【问题讨论】:

  • 是的,我有一个测试可以确认这种行为 - 它还允许我使用调试器检查整个事情,这就是为什么我要分发内部的 TransactionStatus 对象一个保存点,但仍然表现得好像它不在一个中,并回滚整个 tx。不,我不能更改 methodA 的 tx,因为它不是唯一调用它的地方,在那些地方它是正确的策略。切换到MANDATORY 不会改变任何东西,在这种情况下(因为doSomeWork(),tx 将存在),但现在抛出异常会破坏其他情况。
  • methodA 进行回滚时,我应该恢复BeanB 的内部状态,但仍然应该能够提交OtherBean::doSomeWork 在事后所做的任何事情。整个 tx 回滚 IFF OtherBean 回滚。现在,这不是正在发生的事情 - 当BeanB 回滚时,OtherBean::doSomeWork 也会回滚。你说得对,在这个例子中 methodA() 的属性无关紧要 - 但它们在整个系统中很重要,因为 methodA 也可以从其他地方单独调用。

标签: java spring jdbc spring-transactions


【解决方案1】:

如此错误票中所述:https://github.com/spring-projects/spring-framework/issues/11234

对于

在这个事务中,我正在处理几个任务。如果在单个任务期间发生错误,我不希望回滚整个事务,因此我将任务处理包装在另一个事务边界中,传播 PROPAGATION_NESTED。

在任务处理期间,当调用以 PROPAGATION_REQUIRED 的事务边界定义的现有服务方法时,就会出现问题。从这些方法抛出的任何运行时异常都会导致底层连接被标记为仅回滚,而不是尊重当前的父事务嵌套传播。

[...]

从 Spring Framework 5.0 开始,嵌套事务在回滚到保存点时解决其仅回滚状态,不再将其应用于全局事务。

在旧版本上,建议的解决方法是在这种情况下将globalRollbackOnParticipationFailure 切换为false

但是,即使对于 Spring5,我在重现问题时也注意到,嵌套事务可能会回滚,包括在 methodB() 的 catch 块中完成的所有事情。因此,您的恢复代码可能无法在 methodB() 中工作,具体取决于您的恢复情况。如果 methodA() 不是事务性的,那将不会发生。只是需要注意的事情。

更多细节可以在这里找到:https://github.com/spring-projects/spring-framework/issues/8135

【讨论】:

  • 有趣。在那个链接中,他们谈论为此对AbstractPlatformTransactionManager 进行改进,但与我当前的版本相比,我在该课程中没有看到任何相关内容。也许这就是我。无论如何,你的回答对我来说已经足够了,因为它证实了这确实是 Spring 部分的不直观行为,而不仅仅是我对我的假设有误。
猜你喜欢
  • 1970-01-01
  • 2013-12-10
  • 1970-01-01
  • 2012-09-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-16
  • 1970-01-01
相关资源
最近更新 更多