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

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:

  1. 原事务中的Photo对象仅存在于原线程的Hibernate一级缓存中,未提交到数据库
  2. 异步线程的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 23:05:04