双重嵌套事务的回滚绕过保存点

Rollback for doubly nested transaction bypasses savepoint

和标题说的不完全一样,但是差不多。考虑这些 Spring 个豆子:

@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. MainWorkerBeandoSomeWork 开始时,创建新事务
  2. methodB启动时,事务管理器正确初始化一个保存点并将其交给TransactionInterceptor
  3. methodA 启动时,事务管理器再次看到 Propagation.REQUIRED,并再次发出对实际 JDBC 事务的干净引用,它不知道保存点

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

目前,我不能考虑将BeanA的交易改为Propagation.NESTED。不可否认,看起来它会让我有更多的本地回滚,但它太本地化了,因为据我了解,Spring 然后将有两个保存点,并且只回滚 BeanA 保存点,不是我想要的 BeanB 一个。

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

现在我们正在使用 Spring 4.3.24,但我已经浏览了他们的代码,无法发现任何相关更改,所以我认为升级对我没有帮助。

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

对于 spring 版本 < 5.0,在描述的情况下,全局事务设置为 'rollback-only'。

In this transaction I am processing several tasks. If an error should occur during a single task, I do not want the whole transaction to be rolled back, therefore I wrap the task processing in another transaction boundary with a propagation of PROPAGATION_NESTED.

The problem comes when, during task processing, calls are made to existing service methods defined with a transaction boundary of PROPAGATION_REQUIRED. Any runtime exceptions thrown from these methods cause the underlying connection to be marked as rollback-only, rather than respecting the current parent transaction nested propagation.

[...]

As of Spring Framework 5.0, nested transactions resolve their rollback-only status on a rollback to a savepoint, not applying it to the global transaction anymore.

On older versions, the recommended workaround is to switch globalRollbackOnParticipationFailure to false in such scenarios.

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

可在此处找到更多详细信息:https://github.com/spring-projects/spring-framework/issues/8135