我的项目使用 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
日志成功。但当发生取消事件时,有时日志中的事件顺序是这样的。
doOnSuccessOrError()
,并且有东西被记录在 onCancel()
所以取消似乎发生在业务方法执行的 "正中间"。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行为改成了回滚),有了这个补丁,我的测试不再产生任何不一致的数据了。
事实证明,确实存在一个反应式的Spring事务由于意外取消而半途提交的可能性。https:/github.comspring-projectsspring-frameworkissues25091
这个问题是由于 "取消时提交 "的策略造成的,Spring的人正计划在Spring 5.3中把它改成 "取消时回滚 "策略。Spring的人正计划在Spring 5.3中把它改成 "回滚-取消 "策略。目前,可以选择的方法有。
spring-tx
库,并进行如下修正 https:/github.comrpuchspring-frameworkcommit95c2872c0c3a8bebec06b413001148b28bc78f2a。 转为 "取消时回滚 "策略,以避免这种不愉快的意外。但这将意味着完全有效的Reactor操作符(使用取消信号作为其正常功能的一部分)将变得无法在事务操作符的下游使用(因为由它们例行发出的取消将回滚事务)。下面是一篇关于此事的文章。https:/blog.rpuch.com20200525spring-reactive-transactions-atomicity-violation.html。 (声明:我是文章的作者)。