同一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
相关产品推荐
相关产品推荐

