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

同一JPA数据源切换读写/只读权限数据库用户的实现方案咨询

实现方案:基于Spring AbstractRoutingDataSource的动态数据源路由

该方案无需重复定义@Entity实体,仅需配置两个指向同一数据库的数据源、实现路由逻辑、配合注解标识读写操作即可满足需求。


步骤1:配置双账号数据源参数

在application.yml/application.properties中配置读写、只读两个数据源的连接信息,两者的数据库连接地址完全一致,仅账号密码不同:

spring:
  datasource:
    # 读写权限账号数据源
    master:
      jdbc-url: jdbc:mysql://你的数据库地址:3306/库名?useUnicode=true&characterEncoding=utf8
      username: 读写账号
      password: 读写账号密码
      driver-class-name: com.mysql.cj.jdbc.Driver
    # 只读权限账号数据源
    slave:
      jdbc-url: jdbc:mysql://你的数据库地址:3306/库名?useUnicode=true&characterEncoding=utf8
      username: 只读账号
      password: 只读账号密码
      driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: none
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect

步骤2:定义数据源上下文持有类

用ThreadLocal存储当前线程使用的数据源类型,避免多线程场景下数据源类型串用:

public class DataSourceContextHolder {
    private static final ThreadLocal<String> DATASOURCE_CONTEXT = new ThreadLocal<>();

    public static void setDataSourceType(String type) {
        DATASOURCE_CONTEXT.set(type);
    }

    public static String getDataSourceType() {
        return DATASOURCE_CONTEXT.get();
    }

    public static void clear() {
        DATASOURCE_CONTEXT.remove();
    }

    // 数据源类型常量
    public static final String MASTER = "master";
    public static final String SLAVE = "slave";
}

步骤3:实现动态数据源路由类

继承Spring提供的AbstractRoutingDataSource,重写方法指定当前线程要使用的数据源:

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }
}

步骤4:配置动态数据源为默认数据源

替换Spring默认的单数据源配置,将两个数据源注册到动态路由中,仅需指定一次实体类所在包:

@Configuration
public class DynamicDataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean
    public DataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(DataSourceContextHolder.MASTER, masterDataSource);
        dataSourceMap.put(DataSourceContextHolder.SLAVE, slaveDataSource);

        DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
        routingDataSource.setTargetDataSources(dataSourceMap);
        // 默认使用读写数据源
        routingDataSource.setDefaultTargetDataSource(masterDataSource);
        return routingDataSource;
    }

    // 配置JPA实体管理器,仅需指定一次实体包路径
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, DataSource dynamicDataSource) {
        return builder
                .dataSource(dynamicDataSource)
                .packages("你的实体类所在包路径,例如com.xxx.entity")
                .persistenceUnit("default")
                .build();
    }

    @Bean
    public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory.getObject());
    }
}

步骤5:实现数据源自动切换

推荐通过AOP拦截事务注解实现自动切换,无需修改现有业务代码:

@Aspect
@Component
public class DataSourceSwitchAspect {
    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Transactional transactional = signature.getMethod().getAnnotation(Transactional.class);
        try {
            if (transactional.readOnly()) {
                // 只读事务切只读账号
                DataSourceContextHolder.setDataSourceType(DataSourceContextHolder.SLAVE);
            } else {
                // 非只读事务切读写账号
                DataSourceContextHolder.setDataSourceType(DataSourceContextHolder.MASTER);
            }
            return point.proceed();
        } finally {
            // 执行完成清除上下文,避免线程复用导致异常
            DataSourceContextHolder.clear();
        }
    }
}

注意事项

  • 事务嵌套场景下如果需要切换数据源,需调整事务传播属性,避免内层事务复用外层事务的数据库连接
  • 两个数据源可独立配置连接池参数,互不影响
  • 现有@Entity、Repository、业务代码无需做任何修改,完全兼容原有逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 04:45:04