TransactionSynchronizationManager只读状态异常,事务动态路由求助
我尝试将事务动态路由至READ_WRITE或READ_ONLY数据源,但在继承AbstractRoutingDataSource的TransactionRoutingDataSource类中,调用TransactionSynchronizationManager.isCurrentTransactionReadOnly()始终返回false。
我曾通过自定义注解@UseReadReplica结合切面ReadReplicaAspect设置上下文,但上下文设置时机晚于事务路由时机,无法生效。使用默认值(DataSourceType.READ_WRITE与DataSourceType.READ_ONLY)测试时,事务可正确路由至对应数据库,只需在TransactionRoutingDataSource中获取正确的只读状态即可解决问题。
TransactionRoutingDataSource
package com.example.wsappsiav.MultipleDatabase.Configuration.Teste; import com.example.wsappsiav.MultipleDatabase.Properties.DatabaseContextHolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.transaction.support.TransactionSynchronizationManager; public class TransactionRoutingDataSource extends AbstractRoutingDataSource { private static final Logger LOGGER = LoggerFactory.getLogger(TransactionRoutingDataSource.class); @Override protected Object determineCurrentLookupKey() { boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); LOGGER.info("Routed to : " + DatabaseContextHolder.getEnvironment()); LOGGER.info("isReadOnly : " + isReadOnly); return DatabaseContextHolder.getEnvironment(); } }
ReadReplicaAspect
package com.example.wsappsiav.MultipleDatabase; import com.example.wsappsiav.MultipleDatabase.Configuration.Teste.DataSourceType; import com.example.wsappsiav.MultipleDatabase.Properties.DatabaseContextHolder; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.transaction.support.TransactionSynchronizationManager; @Aspect @Component public class ReadReplicaAspect { private static final Logger LOGGER = LoggerFactory.getLogger(ReadReplicaAspect.class); @Around("@annotation(com.example.wsappsiav.MultipleDatabase.annotations.UseReadReplica)") public Object routeToReadReplica(ProceedingJoinPoint joinPoint) throws Throwable { LOGGER.info("Setting database context to " + DataSourceType.READ_ONLY); TransactionSynchronizationManager.setCurrentTransactionReadOnly(true); LOGGER.info("Transaction Mananger: " + TransactionSynchronizationManager.isCurrentTransactionReadOnly()); DatabaseContextHolder.set(DataSourceType.READ_ONLY); return joinPoint.proceed(); } }
ClienteService
@Service public class ClienteService { @Autowired private ClienteRepository clienteRepository; @Transactional(readOnly = true, propagation = Propagation.MANDATORY) @UseReadReplica public List<Cliente> listarTodosClientes() { return clienteRepository.findAll(); } }
ReplicaDataSource
package com.example.wsappsiav.MultipleDatabase.Configuration.replica; import org.springframework.beans.factory.annotation.Qualifier; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface ReplicaDataSource { }
日志信息
2024-05-14T10:13:08.676-03:00 INFO 15568 --- [ws-app-siav] [nio-8080-exec-1] c.e.w.M.C.T.TransactionRoutingDataSource : Routed to : null 2024-05-14T10:13:08.676-03:00 INFO 15568 --- [ws-app-siav] [nio-8080-exec-1] c.e.w.M.C.T.TransactionRoutingDataSource : isReadOnly : false 2024-05-14T10:13:08.868-03:00 INFO 15568 --- [ws-app-siav] [nio-8080-exec-1] c.e.w.M.ReadReplicaAspect : Setting database context to READ_ONLY 2024-05-14T10:13:08.868-03:00 INFO 15568 --- [ws-app-siav] [nio-8080-exec-1] c.e.w.M.ReadReplicaAspect : Transaction Mananger: true 2024-05-14T10:13:08.947-03:00 DEBUG 15568 --- [ws-app-siav] [nio-8080-exec-1] org.hibernate.SQL : select c1_0.id,c1_0.cliente_nome,c1_0.cpf_cnpj,c1_0.email,c1_0.endereco,c1_0.telefone from bd_siav.cliente c1_0 Hibernate: select c1_0.id,c1_0.cliente_nome,c1_0.cpf_cnpj,c1_0.email,c1_0.endereco,c1_0.telefone from bd_siav.cliente c1_0
application.properties
spring.aop.proxy-target-class=true spring.jpa.properties.org.hibernate.flushMode = MANUAL spring.jpa.show-sql=true logging.level.org.hibernate.SQL=DEBUG spring.jpa.hibernate.ddl-auto=none spring.sql.init.mode=always spring.sql.init.continue-on-error=true
核心问题是切面执行时机晚于事务初始化和数据源路由,导致TransactionRoutingDataSource获取只读状态时,切面还没设置上下文。以下是几种可行的解决方式:
1. 调整切面优先级,确保在事务切面之前执行
Spring的@Transactional切面默认优先级为Ordered.LOWEST_PRECEDENCE,需让自定义切面优先级更高,提前完成上下文设置:
@Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE + 10) // 优先级高于事务切面 public class ReadReplicaAspect { // 原有代码不变 }
调整后需验证日志顺序,确保切面日志先于TransactionRoutingDataSource的路由日志打印。
2. 直接从@Transactional注解提取只读状态
既然方法已标注@Transactional(readOnly = true),可直接通过反射获取当前方法的注解状态,省去自定义注解和切面:
@Override protected Object determineCurrentLookupKey() { StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); for (StackTraceElement element : stackTrace) { try { Class<?> clazz = Class.forName(element.getClassName()); Method method = clazz.getMethod(element.getMethodName()); Transactional txAnnotation = method.getAnnotation(Transactional.class); if (txAnnotation != null && txAnnotation.readOnly()) { LOGGER.info("isReadOnly : true"); return DataSourceType.READ_ONLY; } } catch (Exception e) { continue; } } LOGGER.info("isReadOnly : false"); return DataSourceType.READ_WRITE; }
3. 用TransactionSynchronization提前绑定上下文
若坚持使用自定义注解,可通过注册事务同步器,在事务初始化阶段设置上下文:
@Around("@annotation(com.example.wsappsiav.MultipleDatabase.annotations.UseReadReplica)") public Object routeToReadReplica(ProceedingJoinPoint joinPoint) throws Throwable { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void beforeCommit(boolean readOnly) { TransactionSynchronizationManager.setCurrentTransactionReadOnly(true); DatabaseContextHolder.set(DataSourceType.READ_ONLY); } }); return joinPoint.proceed(); }
注意:此方式需确保事务传播行为(如MANDATORY)对应的事务已存在。
内容的提问来源于stack exchange,提问作者Eric Borges

