Grails 4.0.10中Quartz Job抛出会话关联异常的解决求助
我有一个基于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

