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

Grails 5.2跨多数据源查询时的事务异常问题

Grails 5.2混合多租户配置事务异常问题

问题场景

Grails 5.2应用采用混合多租户架构:

  • 部分Domain映射至默认数据源,无多租户实现(存储配置、用户、角色等通用数据)
  • 另一部分Domain实现MultiTenant接口,对应客户独立物理数据库

在标记@Transactional的控制器方法中同时查询两类Domain时,默认数据源会抛出**"no transaction is in progress"**异常。移除@Transactional注解,或用withTransaction块包裹默认数据源查询则可正常运行。该场景在Grails 3.3中可正常工作,升级至Grails 5后出现问题。

配置文件(application.yml)

grails:
    profile: web
    codegen:
        defaultPackage: bilce
    gorm:
        reactor:
            events: false
        multiTenancy:
            mode: DATABASE
            tenantResolverClass: org.grails.datastore.mapping.multitenancy.web.SessionTenantResolver
---
hibernate:
    cache:
        queries: false
        use_second_level_cache: false
        use_query_cache: false
        region:
            factory_class: 'org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory'
    hbm2ddl:
        jdbc_metadata_extraction_strategy: individually
---
dataSource:
    pooled: true
    jmxExport: true
    driverClassName: org.h2.Driver
    username: sa
    password: ''
---
environments:
    development:
        dataSource:
            dbCreate: create-drop
            url: jdbc:h2:mem:defaultDB;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE

        dataSources:
            dataSource_customer_01:
                dbCreate: create-drop
                url: jdbc:h2:mem:customer_01_DB;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE

            dataSource_customer_02:
                dbCreate: create-drop
                url: jdbc:h2:mem:customer_02_DB;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE

            dataSource_customer_03:
                dbCreate: create-drop
                url: jdbc:h2:mem:customer_03_DB;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE

Domain定义

1. 默认数据源Domain(无多租户)

class DefaultDatasourceDomain {

    String description

    static constraints = {
    }

    static mapping = {
        datasource ConnectionSource.DEFAULT
    }

}

2. 租户数据源Domain(实现MultiTenant)

class CustomerDatasourceDomain implements MultiTenant<CustomerDatasourceDomain> {

    String description

    static constraints = {
    }

    static mapping = {
    }

}

控制器代码

class TestController {

    def index() {
        session.setAttribute(SessionTenantResolver.ATTRIBUTE, "dataSource_customer_01")
        redirect action: "doWork"
    }

    // 标记@Transactional时会触发默认数据源的事务异常
    @Transactional
    @CurrentTenant
    def doWork() {
        println("----------------------------------")
        println("DefaultDatasourceDomain contains " + DefaultDatasourceDomain.count() + " records")
        println("CustomerDatasourceDomain contains " + CustomerDatasourceDomain.count() + " records")
        println("----------------------------------")

        redirect uri: "/"
    }
}

异常堆栈信息

Caused by: javax.persistence.TransactionRequiredException: no transaction is in progress
at org.hibernate.internal.AbstractSharedSessionContract.checkTransactionNeededForUpdateOperation(AbstractSharedSessionContract.java:445)
at org.hibernate.internal.SessionImpl.checkTransactionNeededForUpdateOperation(SessionImpl.java:3483)
at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1399)
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1394)
at org.springframework.orm.hibernate5.SessionFactoryUtils.flush(SessionFactoryUtils.java:113)
at org.springframework.orm.hibernate5.SpringSessionSynchronization.beforeCommit(SpringSessionSynchronization.java:95)
at org.springframework.transaction.support.TransactionSynchronizationUtils.triggerBeforeCommit(TransactionSynchronizationUtils.java:97)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerBeforeCommit(AbstractPlatformTransactionManager.java:916)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:727)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:152)
at grails.gorm.transactions.GrailsTransactionTemplate.execute(GrailsTransactionTemplate.groovy:91)
at it.testapp.TestController.$mt__doWork(TestController.groovy)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:107)
at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:323)
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1268)
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1035)
at org.codehaus.groovy.runtime.InvokerHelper.invokePogoMethod(InvokerHelper.java:1029)
at org.codehaus.groovy.runtime.InvokerHelper.invokeMethod(InvokerHelper.java:1012)
at org.codehaus.groovy.runtime.InvokerHelper.invokeMethodSafe(InvokerHelper.java:101)
at it.testapp.TestController$_doWork_closure1.doCall(TestController.groovy)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:107)
at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:323)
at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:274)
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1035)
at groovy.lang.Closure.call(Closure.java:412)
at groovy.lang.Closure.call(Closure.java:428)
at grails.gorm.multitenancy.Tenants$_withId_closure2$_closure6.doCall(Tenants.groovy:265)
at grails.gorm.multitenancy.Tenants$_withId_closure2$_closure6.call(Tenants.groovy)
at org.grails.orm.hibernate.GrailsHibernateTemplate.doExecute(GrailsHibernateTemplate.java:297)
at org.grails.orm.hibernate.GrailsHibernateTemplate.execute(GrailsHibernateTemplate.java:241)
at org.grails.orm.hibernate.GrailsHibernateTemplate.executeWithNewSession(GrailsHibernateTemplate.java:153)
at org.grails.orm.hibernate.GrailsHibernateTemplate.executeWithExistingOrCreateNewSession(GrailsHibernateTemplate.java:207)
at org.grails.orm.hibernate.AbstractHibernateDatastore.withNewSession(AbstractHibernateDatastore.java:369)
at grails.gorm.multitenancy.Tenants$_withId_closure2.doCall(Tenants.groovy:258)
at grails.gorm.multitenancy.Tenants$_withId_closure2.call(Tenants.groovy)
at grails.gorm.multitenancy.Tenants$CurrentTenant.withTenant(Tenants.groovy:358)
at grails.gorm.multitenancy.Tenants.withId(Tenants.groovy:236)
at org.grails.datastore.gorm.services.DefaultTenantService.withCurrent(DefaultTenantService.groovy:71)
at it.testapp.TestController.doWork(TestController.groovy)
... 55 common frames omitted

解决方案

方案1:为默认数据源查询显式绑定事务

将默认数据源的查询逻辑用withTransaction包裹,指定默认数据源:

@Transactional
@CurrentTenant
def doWork() {
    println("----------------------------------")
    // 显式为默认数据源开启事务
    DefaultDatasourceDomain.withTransaction([datasource: ConnectionSource.DEFAULT]) {
        println("DefaultDatasourceDomain contains " + DefaultDatasourceDomain.count() + " records")
    }
    println("CustomerDatasourceDomain contains " + CustomerDatasourceDomain.count() + " records")
    println("----------------------------------")

    redirect uri: "/"
}

方案2:指定事务注解的默认数据源

如果方法中默认数据源操作占比高,可直接在@Transactional中指定默认数据源,租户Domain的查询会自动继承@CurrentTenant的上下文:

@Transactional(value = ConnectionSource.DEFAULT)
@CurrentTenant
def doWork() {
    // 此时默认数据源操作在事务内,租户数据源操作由@CurrentTenant上下文处理
    println("----------------------------------")
    println("DefaultDatasourceDomain contains " + DefaultDatasourceDomain.count() + " records")
    println("CustomerDatasourceDomain contains " + CustomerDatasourceDomain.count() + " records")
    println("----------------------------------")

    redirect uri: "/"
}

方案3:配置多事务管理器(复杂场景)

为默认数据源单独配置事务管理器,在需要时指定使用:

  1. 在resources.groovy中注册默认数据源的事务管理器:
import org.grails.orm.hibernate.HibernateTransactionManager
import org.springframework.orm.hibernate5.LocalSessionFactoryBean

beans = {
    defaultTransactionManager(HibernateTransactionManager) {
        sessionFactory = ref('sessionFactory_' + ConnectionSource.DEFAULT)
    }
}
  1. 在控制器方法中指定事务管理器:
@Transactional(value = "defaultTransactionManager")
@CurrentTenant
def doWork() {
    // 业务逻辑
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 04:30:53