Spring反应式事务在取消时被提交,产生部分提交。

问题描述 投票:0回答:1

我的项目使用 spring-data-mongodb,一切都是反应式的。有一个使用声明式事务的事务方法的Bean。相关代码片段如下。

@Configuration
public class Config {

    @Bean
    public ReactiveMongoTransactionManager reactiveMongoTransactionManager() {
        return new ReactiveMongoTransactionManager(reactiveMongoDbFactory());
    }

    ...
}

@Service
public class MyService {
    private final ReactiveMongoOperations mongoOperations;

    ...

    @Transactional
    public Mono<User> saveUser(User user) {
        return mongoOperations.insert(user).then(anotherInsertOnMongoOperations()).thenReturn(user);
    }
}

这里没有什么不寻常的地方

我可以在日志中看到,事务在文档插入之前就开始了,之后它们被提交。

DEBUG o.s.d.m.ReactiveMongoTransactionManager -  About to start transaction for session [ClientSessionImpl@62de8058 id = {"id": {"$binary": "fye2h5JkRh6yL3MTqtC0Xw==", "$type": "04"}}, causallyConsistent = true, txActive = false, txNumber = 1, error = d != java.lang.Boolean].
DEBUG o.s.d.m.ReactiveMongoTransactionManager -  Started transaction for session [ClientSessionImpl@62de8058 id = {"id": {"$binary": "fye2h5JkRh6yL3MTqtC0Xw==", "$type": "04"}}, causallyConsistent = true, txActive = true, txNumber = 2, error = d != java.lang.Boolean].

...插入之后,然后... ...

DEBUG o.s.d.m.ReactiveMongoTransactionManager -  Initiating transaction commit
DEBUG o.s.d.m.ReactiveMongoTransactionManager -  About to commit transaction for session [ClientSessionImpl@62de8058 id = {"id": {"$binary": "fye2h5JkRh6yL3MTqtC0Xw==", "$type": "04"}}, causallyConsistent = true, txActive = true, txNumber = 2, error = d != java.lang.Boolean].

但有时,正如我从数据库的内容中看到的那样,只有第一个插入是持久的,而第二个插入则丢失了。在尝试对这种情况进行建模后,我发现这种 "丢失 "发生在整个反应式管道被取消的时候(并不是每次都是这样,但我能够产生一个测试,以高概率重现这种情况)。

我添加了 .doOnSuccessOrError().doOnCancel() 在我的方法的最后一个操作符后进行一些记录。在 "正常 "情况下(没有取消)。doOnSuccessOrError 日志成功。但当发生取消事件时,有时日志中的事件顺序是这样的。

  1. 一个事务被启动
  2. 发生插入
  3. 取消发生
  4. 毫无 被最终的 doOnSuccessOrError(),并且有东西被记录在 onCancel() 所以取消似乎发生在业务方法执行的 "正中间"。
  5. ...但 事务仍然被提交!

TransactionAspectSupport.ReactiveTransactionSupport 包含以下代码(用于本例)。

                            return Mono.<Object, ReactiveTransactionInfo>usingWhen(
                                    Mono.just(it),
                                    txInfo -> {
                                        try {
                                            return (Mono<?>) invocation.proceedWithInvocation();
                                        }
                                        catch (Throwable ex) {
                                            return Mono.error(ex);
                                        }
                                    },
                                    this::commitTransactionAfterReturning,
                                    (txInfo, err) -> Mono.empty(),
                                    this::commitTransactionAfterReturning)

最后一个参数是 onCancel 处理程序,这意味着在取消时,交易实际上被提交。

这意味着在取消时,交易实际上被提交。

问题是:为什么?当由于反应式管道外部的原因而发生取消时,事务中的一些操作可能已经完成,而一些操作还没有完成(而且永远不会完成)。在这样的时刻提交会产生部分提交,这违反了原子性要求。

似乎更合理的做法是发起回滚。但我想,作者在 spring-tx 故意做了这个选择。我想知道,这是什么原因?

P.S.为了验证我的观点,我补上了 spring-tx 5.2.3(顺便说一下,项目用的就是这个版本),这样代码就像这样了。

                            return Mono.<Object, ReactiveTransactionInfo>usingWhen(
                                    Mono.just(it),
                                    txInfo -> {
                                        try {
                                            return (Mono<?>) invocation.proceedWithInvocation();
                                        }
                                        catch (Throwable ex) {
                                            return Mono.error(ex);
                                        }
                                    },
                                    this::commitTransactionAfterReturning,
                                    (txInfo, err) -> Mono.empty(),
                                    this::rollbackTransactionDueToCancel)

    private Mono<Void> rollbackTransactionDueToCancel(@Nullable ReactiveTransactionInfo txInfo) {
        if (txInfo != null && txInfo.getReactiveTransaction() != null) {
            if (logger.isDebugEnabled()) {
                logger.debug("Rolling transaction back for [" + txInfo.getJoinpointIdentification() + "] due to cancel");
            }
            return txInfo.getTransactionManager().rollback(txInfo.getReactiveTransaction());
        }
        return Mono.empty();
    }

(基本上,只是把on-cancel行为改成了回滚),有了这个补丁,我的测试不再产生任何不一致的数据了。

java spring reactive-programming spring-data-mongodb spring-transactions
1个回答
1
投票

事实证明,确实存在一个反应式的Spring事务由于意外取消而半途提交的可能性。https:/github.comspring-projectsspring-frameworkissues25091

这个问题是由于 "取消时提交 "的策略造成的,Spring的人正计划在Spring 5.3中把它改成 "取消时回滚 "策略。Spring的人正计划在Spring 5.3中把它改成 "回滚-取消 "策略。目前,可以选择的方法有。

  1. 如果你的事务中包含了多个写法,请使用自定义构建的Spring 5. spring-tx 库,并进行如下修正 https:/github.comrpuchspring-frameworkcommit95c2872c0c3a8bebec06b413001148b28bc78f2a。 转为 "取消时回滚 "策略,以避免这种不愉快的意外。但这将意味着完全有效的Reactor操作符(使用取消信号作为其正常功能的一部分)将变得无法在事务操作符的下游使用(因为由它们例行发出的取消将回滚事务)。
  2. 如果你所有的事务都有最多一次写入,那么你可以安全地使用未打补丁的Spring版本。但请注意,Spring的人(目前)将在5.3中翻转这个政策。

下面是一篇关于此事的文章。https:/blog.rpuch.com20200525spring-reactive-transactions-atomicity-violation.html。 (声明:我是文章的作者)。

© www.soinside.com 2019 - 2024. All rights reserved.