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

Grails 4.0.10中Quartz Job抛出会话关联异常的解决求助

问题:Grails 4.0.10 Quartz Job 抛出「Illegal attempt to associate a collection with two open sessions」异常

我有一个基于Grails 4.0.10开发的赛事注册应用,在Quartz Job执行号码布分配时抛出Illegal attempt to associate a collection with two open sessions异常,异常出现在BibsAssigner Job中。该Job由PaymentController调用RegistrationService的activateRegistrations方法触发,负责为注册用户分配号码布。

RegistrationService 代码

@Transactional
void activateRegistrations(SaleInvoice invoice){
    Assert.notNull(invoice, 'SaleInvoice cannot be null when activating registrations.')
    List<Registration> registrations = this.findRegistrationsBySaleInvoice(invoice)
    
    def mp = [:]
    
    registrations.each{
        it.status = EntityStatus.ACTIVE

        if (it.race.capacity) {
            it.race.capacity?.allocated += 1
            it.race.capacity?.save()
        }

        if (it.compositeEvent.capacity) {
            it.compositeEvent.capacity?.allocated += 1
            it.compositeEvent.capacity?.save()
        }

        if (it.registrationEntry?.template?.capacity) {
            it.registrationEntry.template.capacity?.allocated += 1
            it.registrationEntry.template.capacity?.save()
        }

        it.acceptedRunnercardDisclaimer = it.waiver.runnercardDisclaimer.disclaimer.content
        it.acceptedEventDisclaimer = it.waiver.eventDisclaimer.disclaimer.content

        it.save()
        
        if(!mp[it.race.id]){
            mp[it.race.id] = [it]
        }
        else{
            mp[it.race.id].add(it)
        }
                    
        if(it.errors?.errorCount > 0){
            log.error "error when activating registrations " + it.errors
        }
    }

    if(invoice.compositeEvent.assignBibNumbers){
        BibsAssignerJob.triggerNow([regmap: mp])
    }
}

BibsAssigner Job 代码

class BibsAssignerJob {

    static concurrent = false

    def mailService
    def bibsService

    static triggers = {
//      simple repeatInterval: 5000l // execute job once in 5 seconds
    }

    //mp is map of race id -> [registration1, registration2]
    void assignBibNumbers(mp){
        Race.withTransaction {
            try {
                def bibRangeEventBased = true

                for (r in mp) {
                    def rac = Race.get(r.key)
                    if (rac.bibsRange) {
                        bibRangeEventBased = false
                        break
                    }
                }

                if (!bibRangeEventBased) {
                    //first assign bibs to races with range assignment
                    for (r in mp) {
                        def rac = Race.get(r.key)
                        if (rac.bibsRange) {
                            def regs = r.value
                            def bibs = bibsService.convertRangeStringToRangeList(rac.bibsRange)
                            bibs.removeAll(bibsService.getAllBibsAssigned(rac.compositeEvent))

                            for (int i = 0; i < regs.size(); i++) {
                                if (i < bibs.size()) {
                                    if (regs[i].compositeEvent.assignBibNumbers) {
                                        def registration = grails.util.Holders.applicationContext.getBean('proxyHandler').unwrapIfProxy(regs[i])
                                        registration.raceParticipant.bibNumber = bibs[i]
                                        registration.markDirty('raceParticipant')
                                        registration.save(flush: true)
                                    }
                                }
                            }
                        }
                    }
                } else {
                    //assign bibs based on event range
                    def regs = []
                    for (r in mp) {
                        regs += r.value
                    }
                    def bibs = bibsService.convertRangeStringToRangeList(regs[0].compositeEvent.bibsRange)
                    bibs.removeAll(bibsService.getAllBibsAssigned(regs[0].compositeEvent))

                    for (int i = 0; i < regs.size(); i++) {
                        if (i < bibs.size()) {
                            if (regs[i].compositeEvent.assignBibNumbers) {
                                def registration = grails.util.Holders.applicationContext.getBean('proxyHandler').unwrapIfProxy(regs[i])
                                registration.raceParticipant.bibNumber = bibs[i]
                                registration.markDirty('raceParticipant')
                                registration.save(flush: true)
                            }
                        }
                    }
                }
            }
            catch (Exception e) {
                def regs = mp.values().toList().flatten()
                def ev = regs.first().compositeEvent.name + "(${regs.first().compositeEvent.id})"
                def pars = ""

                for(reg in regs){
                    pars += reg.participant.name
                    pars += ", "
                }

                println "************************************************************************************************************************************"
                println "Bibs assignment failed for ${ev} and participants ${pars}. Exception thrown was ${e}"
                println "************************************************************************************************************************************"
            }
        }
    }

    def execute(context) {
        Thread.sleep(20000)
        println "Running Bib Assigner"
        def mp = context.mergedJobDataMap.regmap

        if (mp.size() == 0){
            return
        }
        assignBibNumbers(mp)
    }
}

异常信息

org.springframework.orm.hibernate5.HibernateSystemException: Illegal attempt to associate a collection with two open sessions. Collection : [com.runnercard.registration.Registration.inventory#382465]; nested exception is org.hibernate.HibernateException: Illegal attempt to associate a collection with two open sessions. Collection : [com.runnercard.registration.Registration.inventory#382465]

异常无法稳定复现,推测和并发有关,求解决方法。


解决方案

核心原因

异常本质是Hibernate持久化对象被两个不同的Session关联:

  • RegistrationService中的registrations对象是在Service事务Session中加载的,触发Job时直接传递这些对象给Quartz。
  • Quartz Job运行在独立线程,会创建自己的Hibernate Session,此时修改并保存原Service线程加载的对象,会导致同一个集合(比如Registration.inventory)被两个Session关联,触发冲突。

具体修复步骤

1. 传递ID而非完整对象到Job

不要直接传递Registration实体对象,改为传递ID集合,在Job中重新加载对象。修改RegistrationService的activateRegistrations方法:

// 原代码:mp[it.race.id].add(it)
// 替换为:
mp[it.race.id].add(it.id) // 只存ID,不存实体

2. 在Job中重新加载实体

修改BibsAssignerJob的assignBibNumbers方法,通过ID重新获取Registration对象,确保使用Job线程自己的Session:

// 以赛事分配分支为例,其他分支同理修改
for (int i = 0; i < regIds.size(); i++) {
    if (i < bibs.size()) {
        if (rac.compositeEvent.assignBibNumbers) {
            // 通过ID重新加载,使用当前Session
            def registration = Registration.get(regIds[i])
            if(registration){
                registration.raceParticipant.bibNumber = bibs[i]
                registration.save(flush: true)
            }
        }
    }
}

3. 移除不必要的unwrap操作

原代码中的unwrapIfProxy操作多余,重新加载的对象已经是当前Session管理的实例,无需解包。

4. 优化事务边界

将Job中的Race.withTransaction替换为@Transactional注解(添加在assignBibNumbers方法上),让Grails自动管理事务和Session,避免手动事务的潜在问题:

@Transactional
void assignBibNumbers(mp){
    // 原代码逻辑,去掉Race.withTransaction包裹
}

5. 删除无用的Thread.sleep

execute方法中的Thread.sleep(20000)完全没必要,会浪费资源且可能加剧Session冲突,直接删除即可。

额外建议

  • 跨线程操作实体时,始终传递ID并重新加载,这是Grails中避免Session冲突的标准做法。
  • 检查所有save()调用,确保在事务边界内执行,避免出现游离对象。
  • 开启Hibernate的org.hibernate日志,复现异常时查看Session创建和销毁的详细流程,精准定位冲突点。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 08:20:07