You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何测试R2DBC PostgreSQL与Webflux中SELECT FOR UPDATE的并行事务

问题分析与解决方案

你的核心问题在于事务管理器与协程Repository不兼容,导致for update skip locked的行锁定逻辑未生效,两个并行查询拿到了相同数据。以下是具体原因和修正方案:

核心问题点

你使用了ReactiveTransactionManager(反应式事务管理器)来管理CoroutineCrudRepository的事务,但Spring Data协程需要**CoroutineTransactionManager**才能将事务上下文正确绑定到协程执行环境中。之前的自定义DelayedTransactionalOperator基于反应式API实现,无法适配协程上下文,导致查询根本没在事务内执行,自然不会触发行锁定。

修正后的测试方案

1. 依赖调整与事务管理器注入

确保你的测试类中注入CoroutineTransactionManager而非ReactiveTransactionManager:

@Autowired
lateinit var taskRepo: TaskRepository

@Autowired
lateinit var coroutineTxManager: CoroutineTransactionManager

2. 完整测试代码

import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.springframework.transaction.annotation.Isolation
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.reactive.TransactionDefinition
import org.springframework.transaction.reactive.transactionDefinition
import org.springframework.transaction.support.withTransaction
import java.time.Instant

@Test
fun `should select distinct tasks in parallel transactions`() = runBlocking {
    // 前置准备:插入10个NEW状态的任务,保证两个事务各能拿到5个不同数据
    val testTasks = List(10) { Task(status = "NEW", creationDate = Instant.now()) }
    taskRepo.saveAll(testTasks).collect()

    var trx1Tasks: List<Task> = emptyList()
    var trx2Tasks: List<Task> = emptyList()

    // 启动两个并行事务
    val trxJob1 = launch {
        trx1Tasks = withTransaction(coroutineTxManager, transactionDefinition {
            isolationLevel = Isolation.REPEATABLE_READ
            propagation = Propagation.REQUIRES_NEW
        }) {
            val fetchedTasks = taskRepo.selectForProcessing(5).toList()
            // 延迟提交事务,确保trx2执行查询时,trx1已锁定选中行
            delay(300)
            fetchedTasks
        }
    }

    val trxJob2 = launch {
        trx2Tasks = withTransaction(coroutineTxManager, transactionDefinition {
            isolationLevel = Isolation.REPEATABLE_READ
            propagation = Propagation.REQUIRES_NEW
        }) {
            taskRepo.selectForProcessing(5).toList()
        }
    }

    // 等待两个事务执行完成
    trxJob1.join()
    trxJob2.join()

    // 断言验证:两个列表无交集,且各自拿到5条数据
    assertEquals(5, trx1Tasks.size)
    assertEquals(5, trx2Tasks.size)
    assertTrue(trx1Tasks.intersect(trx2Tasks.toSet()).isEmpty())
}

3. 关键说明

  • 事务上下文绑定:使用withTransaction扩展函数(Spring提供的协程事务API),确保查询在正确的事务上下文内执行,for update skip locked的行锁定逻辑生效。
  • 并行执行控制:用launch启动两个独立协程事务,保证它们并行执行;trx1延迟提交,确保trx2执行查询时,trx1已经锁定了选中的行。
  • 数据准备:提前插入10条NEW状态的任务,避免因数据不足导致两个事务拿到相同数据。

为什么之前的自定义Operator无效?

你的DelayedTransactionalOperator基于反应式TransactionalOperator实现,而协程Repository的查询是在协程上下文而非反应式流上下文执行的,两者的上下文无法互通,导致查询脱离了事务控制,for update skip locked的锁定规则根本没被触发,最终两个查询返回了相同结果。

内容的提问来源于stack exchange,提问作者dippy tippy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 08:25:12