Kotlin协程中使用withContext切换安全上下文时如何保留事务?
问题原因
你遇到的问题本质是Reactor Context被完全覆盖,导致事务上下文丢失:
- Spring的反应式事务是通过Reactor Context传递事务状态的,
@Transactional注解会在方法执行时将事务相关的上下文存入Reactor Context中。 - 当你直接用
ReactorContext(...)创建新的上下文并传入withContext时,会替换掉原有的整个Reactor Context,包括里面的事务上下文。这就导致withContext块内的数据库操作不在原事务中执行,自然看不到之前repo.save(Some())的未提交数据(原事务未提交时,非原事务的查询无法读取到未提交的脏数据)。 - 而使用
withContext(Dispatchers.IO)时,只是切换了协程的调度器,不会覆盖原有的Reactor Context,事务上下文得以保留,所以查询能正常读取到原事务内的数据。
解决方案
不要直接替换整个Reactor Context,而是在原有上下文基础上添加/更新SecurityContext,确保事务上下文不丢失。有两种常用方式:
方式一:合并原有Reactor Context
在创建新的ReactorContext时,先获取原有的上下文内容,再合并进SecurityContext:
@Transactional suspend fun foo() { val a = repo.save(Some()) println(repo.count()); // 输出1 // 获取当前协程中的原有Reactor Context val originalReactorContext = coroutineContext[ReactorContext]?.context ?: Context.empty() // 合并原有上下文与新的SecurityContext val newContext = originalReactorContext.putAll( ReactiveSecurityContextHolder.withAuthentication( RunAsUserToken( it.userId.toString(), it.userId, it.userId, mutableListOf(), AbstractAuthenticationToken::class.java, ) ) ) withContext(ReactorContext(newContext)) { println(repo.count()); // 输出1,事务上下文保留 } }
方式二:使用reactorContext扩展函数(更简洁)
借助kotlinx-coroutines-reactor提供的扩展函数,直接修改原有Reactor Context:
import kotlinx.coroutines.reactor.reactorContext @Transactional suspend fun foo() { val a = repo.save(Some()) println(repo.count()); // 输出1 withContext(reactorContext { ctx -> // 在原有上下文基础上添加新的SecurityContext ctx.putAll( ReactiveSecurityContextHolder.withAuthentication( RunAsUserToken( it.userId.toString(), it.userId, it.userId, mutableListOf(), AbstractAuthenticationToken::class.java, ) ) ) }) { println(repo.count()); // 输出1,事务上下文保留 } }
关键要点
- 反应式事务依赖Reactor Context传递状态,任何对Reactor Context的替换操作都会导致事务丢失。
- 协程中使用
withContext时,若要修改上下文,务必基于原有上下文进行合并,而非完全替换。
内容的提问来源于stack exchange,提问作者Stuck
相关产品推荐
相关产品推荐

