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的值
被违反的前提假设
- 你默认假设「只要更新逻辑包裹在
Tenants.withId块内,事务提交时就能拿到租户ID」,但discriminator多租户模式下,事务绑定的GORM会话会在事务启动时就关联租户信息,且Tenants.withId的上下文仅在块内有效,块结束后立刻清除,早于你原代码的事务提交时机。 - 假设「异步方法会自动继承父线程的租户上下文」,Grails未做异步线程的租户上下文自动传递,需要手动处理。
内容的提问来源于stack exchange,提问作者sodastream
相关产品推荐
相关产品推荐

