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

Spring事务只读切面未触发及数据源路由失效问题咨询

问题:Spring读写分离切面未触发,数据源路由失效

核心现象

  • 标记@Transactional(readOnly = true)的方法执行时,TransactionReadonlyAspect切面逻辑完全不触发(控制台无Aspect executed输出)
  • 所有数据库操作始终默认使用主数据源,从未切换至从库
  • DatabaseContextHolder的ThreadLocal变量始终返回null,导致MasterSlaveRoutingDataSource无法获取正确路由key

相关代码片段

切面类

@Aspect
@Component
@Order(0)
public class TransactionReadonlyAspect {

    @Around("@annotation(transactional)")
    public Object proceed(ProceedingJoinPoint proceedingJoinPoint, org.springframework.transaction.annotation.Transactional transactional) throws Throwable {
        System.out.println("Aspect executed");
        try {
            if (transactional.readOnly()) {
                DatabaseContextHolder.set(DatabaseEnvironment.READONLY);
            }
            return proceedingJoinPoint.proceed();
        } finally {
            DatabaseContextHolder.reset();
        }
    }
}

数据源配置类

package com.programmingsharing.demoreadwriterouting.conf;
    
import com.programmingsharing.demoreadwriterouting.context.DatabaseEnvironment;
import com.programmingsharing.demoreadwriterouting.datasource.MasterSlaveRoutingDataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
    
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
    
@Configuration
public class DataSourceConfiguration {
    
    @Value("${jdbc.master.url}")
    private String mstUrl;
    
    @Value("${jdbc.master.username}")
    private String mstUsername;
    
    @Value("${jdbc.master.password}")
    private String mstPassword;
    
    @Value("${jdbc.slave.url}")
    private String slaveUrl;
    
    @Value("${jdbc.slave.username}")
    private String slaveUsername;
    
    @Value("${jdbc.slave.password}")
    private String slavePassword;
    
    @Bean
    public DataSource dataSource(){
        MasterSlaveRoutingDataSource masterSlaveRoutingDataSource = new MasterSlaveRoutingDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DatabaseEnvironment.UPDATABLE, masterDataSource());
        targetDataSources.put(DatabaseEnvironment.READONLY, slaveDataSource());
        masterSlaveRoutingDataSource.setTargetDataSources(targetDataSources);
    
        // 默认指向主库
        masterSlaveRoutingDataSource.setDefaultTargetDataSource(masterDataSource());
        return masterSlaveRoutingDataSource;
    }
    
    public DataSource slaveDataSource() {
    
        HikariDataSource hikariDataSource = new HikariDataSource();
        hikariDataSource.setJdbcUrl(slaveUrl);
        hikariDataSource.setUsername(slaveUsername);
        hikariDataSource.setPassword(slavePassword);
        return hikariDataSource;
    }
    
    public DataSource masterDataSource() {
        HikariDataSource hikariDataSource = new HikariDataSource();
        hikariDataSource.setJdbcUrl(mstUrl);
        hikariDataSource.setUsername(mstUsername);
        hikariDataSource.setPassword(mstPassword);
        return hikariDataSource;
    }
}

上下文持有类

public class DatabaseContextHolder {

    private static final ThreadLocal<DatabaseEnvironment> CONTEXT = new ThreadLocal<>();

    public static void set(DatabaseEnvironment databaseEnvironment) {
        CONTEXT.set(databaseEnvironment);
    }

    public static DatabaseEnvironment getEnvironment() {
        System.out.println("CONTEXT.get() : " + CONTEXT.get());
        return CONTEXT.get();
    }

    public static void reset() {
        CONTEXT.set(DatabaseEnvironment.UPDATABLE);
    }

}

数据源路由类

public class MasterSlaveRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        System.out.println("DatabaseContextHolder.getEnvironment() : " + DatabaseContextHolder.getEnvironment());
        return DatabaseContextHolder.getEnvironment();
    }
}

解决方案

1. 启用Spring AOP自动代理

在配置类或主启动类上添加@EnableAspectJAutoProxy(proxyTargetClass = true),确保Spring能识别并创建切面代理。同时检查@ComponentScan是否包含切面类所在包路径,避免切面未被扫描加载。

2. 调整切面执行顺序

Spring事务切面默认优先级为Ordered.LOWEST_PRECEDENCE,需让你的切面先于事务切面执行(否则事务绑定数据源后切换ThreadLocal无效)。修改切面的@Order为最高优先级:

@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TransactionReadonlyAspect {
    // 原有代码不变
}

3. 修复切点匹配逻辑

如果@Transactional注解标注在接口方法上,默认JDK动态代理无法拦截接口注解。此时要么启用CGLIB代理(已通过proxyTargetClass = true实现),要么直接在实现类方法上标注@Transactional注解。

4. 完善DatabaseContextHolder逻辑

  • 为getEnvironment()添加默认值,避免未设置时返回null:
public static DatabaseEnvironment getEnvironment() {
    DatabaseEnvironment env = CONTEXT.get();
    System.out.println("CONTEXT.get() : " + env);
    return env != null ? env : DatabaseEnvironment.UPDATABLE;
}
  • 修改reset()方法移除ThreadLocal值,避免内存泄漏:
public static void reset() {
    CONTEXT.remove();
}

5. 确保事务管理启用

在配置类或主启动类上添加@EnableTransactionManagement,否则@Transactional注解不会生效,切面自然无法触发。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 07:25:32