Grails中Promise内无法访问同一事务的Hibernate托管对象问题
Grails异步Promise中的事务性问题
问题现象
在Grails服务中使用Promise异步调用时,出现事务上下文不一致的问题:
ClassifierService标记了@Transactional,事务内已创建Photo对象- 在
Promise任务中调用FaceDataClassifierService时,原事务Session中存在的Photo,在FaceDataClassifierService的Session中无法被查询到
相关代码
ClassifierService
import grails.async.Promise import grails.gorm.transactions.Transactional import static grails.async.Promises.task import static grails.async.Promises.waitAll @Transactional class ClassifierService { FaceDataClassifierService faceDataClassifierService FaceDataService faceDataService def classify(Photo photo) { List promises = [] log.error("$photo exists: ${Photo.countById(photo.id) == 1}") findAllByActive(true).each { Classifier classifier -> promises << classify(photo, classifier) } List<Classification> classifications = waitAll(promises)?.flatten() classifications.each { save(it) } } Promise classify(Photo photo, Classifier classifier) { log.debug("Classifying photo $photo.id with Face Data classifier $classifier.name") log.error("$photo exists in promise: ${Photo.countById(photo.id) == 1}") FaceData faceData = faceDataService.findByPhoto(photo, true) log.debug("Found face date ${faceData?.id} for ${faceData?.photo}") return task { faceDataClassifierService.classify(photo) } } // 省略无关代码 }
FaceDataClassifierService
import grails.gorm.transactions.Transactional @Transactional class FaceDataClassifierService { FaceDataService faceDataService List<Classification> classify(Photo photo) { log.error("$photo exists: ${Photo.countById(photo.id) == 1}") // 省略无关代码 FaceData faceData = faceDataService.findByPhoto(photo, true) log.debug("Photo $photo.id has face data: ${faceData?.id}") if (!faceData) return [] // 省略无关代码 } // 省略无关代码 }
日志输出
ClassifierService : Photo(1427248, ENROUTE) exists: true ClassifierService : Classifying photo 1427248 with Face Data classifier Face Data ClassifierService : Photo(1427248, ENROUTE) exists in promise: true FaceDataService : Photo(1427248, ENROUTE) exists in findByPhoto: true ClassifierService : Found face data 3406 for Photo(1427246, DISCARDED) FaceDataClassifierService : Photo(1427248, ENROUTE) exists: false FaceDataService : Photo(1427248, ENROUTE) exists in findByPhoto: false FaceDataClassifierService : Photo 1427248 has face data: null
问题原因
Grails的@Transactional注解是线程绑定的,Hibernate Session仅存在于当前线程的事务上下文中。而Promise.task()会创建新的线程执行异步逻辑,新线程无法继承原线程的事务上下文和Session:
- 原事务中的
Photo对象仅存在于原线程的Hibernate一级缓存中,未提交到数据库 - 异步线程的
FaceDataClassifierService会创建新的Session,无法访问原线程缓存中的未提交数据,且数据库层面读已提交的隔离级别也看不到未提交的记录
解决方案
方案1:预加载所需数据,避免异步任务查询数据库
在原线程中提前加载好FaceData等依赖数据,直接传递给异步任务,无需在异步服务中再次查询:
- 修改
ClassifierService的异步任务调用逻辑:
Promise classify(Photo photo, Classifier classifier) { log.debug("Classifying photo $photo.id with Face Data classifier $classifier.name") log.error("$photo exists in promise: ${Photo.countById(photo.id) == 1}") // 原线程预加载FaceData FaceData faceData = faceDataService.findByPhoto(photo, true) log.debug("Found face date ${faceData?.id} for ${faceData?.photo}") // 传递预加载的FaceData给异步服务 return task { faceDataClassifierService.classify(faceData) } }
- 修改
FaceDataClassifierService的方法:
List<Classification> classify(FaceData faceData) { if (!faceData) return [] Photo photo = faceData.photo // 直接使用传入的faceData和photo对象执行后续逻辑,无需查询数据库 // ... }
方案2:提前flush并拆分事务(谨慎使用)
如果业务允许,可以在启动异步任务前,将原事务的缓存数据flush到数据库并提交事务,确保异步线程能查询到数据。但此方案会破坏原事务的原子性,若后续保存classifications失败,已提交的Photo无法回滚,仅适合业务上允许拆分事务的场景:
def classify(Photo photo) { // 提前flush数据到数据库 photo.save(flush: true) // 手动提交当前事务 transactionManager.commit(transactionStatus) List promises = [] findAllByActive(true).each { Classifier classifier -> promises << classify(photo, classifier) } List<Classification> classifications = waitAll(promises)?.flatten() // 开启新事务保存分类结果 Classification.withTransaction { classifications.each { save(it) } } }
临时方案说明
当前采用的临时方案存在明显弊端:
- 将
faceDataClassifierService调用移到Promise外:失去异步执行的性能优势 - 重写服务避免数据库操作:破坏代码职责单一性,维护成本上升
内容的提问来源于stack exchange,提问作者SGT Grumpy Pants
相关产品推荐
相关产品推荐

