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

Grails 4多租户异步定时任务事务提交抛出TenantNotFoundException排查

问题根本原因

核心是事务启动时机与租户上下文的生命周期不匹配:
你当前的代码执行顺序为:先调用DocAction.withTransaction开启事务,此时GORM/Hibernate会初始化当前会话的租户上下文,而此时还未进入Tenants.withId块,租户ID为空。后续在事务内执行Tenants.withId时会临时设置租户ID,业务逻辑可以正常执行,但Tenants.withId块执行完成后,Grails会自动清空ThreadLocal存储的租户上下文。等到事务触发提交、执行flush操作时,Hibernate需要校验当前会话的租户ID,此时租户上下文已经被清除,且定时任务不在web请求环境中没有其他租户解析来源,就抛出了TenantNotFoundException。

另外你日志中出现多线程执行审计日志的情况,说明logRecordService.logExpire方法加了@Async注解,Grails默认的租户上下文基于ThreadLocal存储,异步线程不会自动继承父线程的租户ID,这也是潜在的报错点,但你注释审计逻辑后问题依然存在,说明主因还是事务与租户块的顺序错误。

修复方案

将Tenants.withId块移到withTransaction的外层,保证事务开启时租户上下文已经正确设置:

def forceExpireMultiple(InsUser actor, Map docActions) {
    if (log.debugEnabled) log.debug "FORCE_EXPIRE_MULTIPLE_S << ${actor}, ${docActions?.keySet()}"
    docActions.each {entry ->
        String caseId = entry.key
        DocAction docAction = entry.value
        // 先设置租户上下文,再开启事务
        Tenants.withId(docAction.ttag) {
            // Each doc action is expired in its own transaction.
            DocAction.withTransaction {status ->
                if (log.traceEnabled) log.trace "forceExpireMultiple.ttag: ${docAction.ttag}"
                docAction = get(docAction.id)
                doForceExpire(actor, caseId, docAction)
            }
        }
    }
}

如果需要保留审计日志的异步逻辑,需要在调用异步方法时手动传递租户ID,在异步方法入口重新调用Tenants.withId设置上下文。

排查验证点

  • 临时去掉logRecordService方法的@Async注解,先排除异步线程上下文丢失的干扰
  • 在withTransaction块第一行打印Tenants.currentId(),确认事务开启时租户ID已经正确存在
  • 开启GORM的SQL日志,确认所有insert/update语句都正确带上了租户标识字段ttag的值

被违反的前提假设

  1. 你默认假设「只要更新逻辑包裹在Tenants.withId块内,事务提交时就能拿到租户ID」,但discriminator多租户模式下,事务绑定的GORM会话会在事务启动时就关联租户信息,且Tenants.withId的上下文仅在块内有效,块结束后立刻清除,早于你原代码的事务提交时机。
  2. 假设「异步方法会自动继承父线程的租户上下文」,Grails未做异步线程的租户上下文自动传递,需要手动处理。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 04:54:01