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

Grails 4与Spock集成测试并发幂等性失败 Hibernate会话关联异常问题

报错根因

你遇到的org.hibernate.HibernateException: Illegal attempt to associate a collection with two open sessions报错,是Grails 2.x到Grails 4.x的session管理机制变化导致的:
Grails 2.x测试默认会将session绑定到所有衍生线程,而Grails 4.x基于Spring事务上下文,Hibernate Session默认是线程隔离的。你在主线程加载的Thing实例以及关联的KiindUser.paymentInfoByCurrency集合,都绑定了主线程的测试Session,子线程拿到这个关联了其他Session的实体后,哪怕只是读取集合触发懒加载,也会触发Session校验报错。

问题1:Grails 4/Spock 并发测试最佳实践

集成测试验证并发逻辑是完全合理的,尤其是幂等性、并发锁这类依赖真实数据库事务、锁机制的场景,单元测试无法覆盖对应的边界情况,不属于不推荐的实践。通用的最佳实践如下:

  • 不要跨线程传递领域实体:子线程需要用到的实体,一律在子线程内部通过ID查询,禁止传递主线程加载的实体实例,避免跨Session共享实体的问题
  • 明确子线程事务边界:如果待测试的服务方法没有加@Transactional注解,需要主动在子线程逻辑外层声明独立事务,确保操作可以正常提交到数据库
  • 禁用测试类自动回滚:大部分Grails集成测试默认开启自动事务回滚,这种配置下主线程的事务隔离级别会挡住子线程提交的数据,并发测试建议标注@Rollback(false),测试结束后手动清理测试数据
  • 隔离测试数据:每次测试前单独插入本次用的测试数据,不要复用其他测试的存量数据,避免并发测试之间互相干扰
问题2:跨线程Session处理方案

不建议将测试主线程的Session绑定到子线程共享:Hibernate Session本身设计为非线程安全,多线程共享会导致脏读、数据状态错乱等不可预期的问题,风险极高。
推荐采用子线程独立Session的方案,修改后测试代码可以正常读取到子线程提交的所有数据,示例代码如下:

// 测试类顶部标注禁用自动回滚
@Rollback(false)
class YourServiceIntegrationSpec extends IntegrationSpec {
    @Autowired
    TransactionTemplate transactionTemplate

    void testConcurrentIdempotency() {
        given:
        // 只获取待操作实体的ID,不传递主线程加载的实体实例
        Long testThingId = Thing.findAll(someId)[0].id
        int attemptsPerThing = 5
        
        when:
        def pool = Executors.newFixedThreadPool(attemptsPerThing)
        List<Future> futureList = []
        
        for (int i = 0; i < attemptsPerThing; i++) {
            Future future = pool.submit({
                // 子线程开启独立事务,使用当前线程的独立Session
                transactionTemplate.execute { status ->
                    try {
                        // 子线程内部查询所需实体,绑定当前线程的Session
                        Thing thing = Thing.get(testThingId)
                        return serviceToBeTested.functionToBeTestedThatCommitsThingsToDB(thing)
                    } catch (Exception e) {
                        log.error("服务调用异常", e)
                        return null
                    }
                }
            })
            futureList.add(future)
        }
        
        futureList.each { future ->
            def futureResults = future.get()
            assert futureResults == null
        }
        
        pool.shutdown()
        assert pool.awaitTermination(60, TimeUnit.SECONDS)

        // 清理主线程Session缓存,避免读到缓存的旧数据
        sessionFactory.currentSession.clear()
        
        then:
        // 此时查询的是数据库最新提交的状态,可正常校验幂等性
        Thing finalThing = Thing.get(testThingId)
        // 后续幂等性校验断言
    }
}

如果你的待测试服务方法本身已经标注了@Transactional,可以不用额外加TransactionTemplate,只要保证子线程内部查询实体即可。


内容的提问来源于stack exchange,提问作者Tyrel H

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.29 22:54:05