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:配置多事务管理器(复杂场景)
为默认数据源单独配置事务管理器,在需要时指定使用:
- 在
resources.groovy中注册默认数据源的事务管理器:
import org.grails.orm.hibernate.HibernateTransactionManager import org.springframework.orm.hibernate5.LocalSessionFactoryBean beans = { defaultTransactionManager(HibernateTransactionManager) { sessionFactory = ref('sessionFactory_' + ConnectionSource.DEFAULT) } }
- 在控制器方法中指定事务管理器:
@Transactional(value = "defaultTransactionManager") @CurrentTenant def doWork() { // 业务逻辑 }
内容的提问来源于stack exchange,提问作者leleFC
相关产品推荐
相关产品推荐

