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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 21:59:51