Spring Data单Repository多数据源写入及数据库迁移阶段动态切换数据源的方案咨询
Spring Data单Repository多数据源写入及数据库迁移阶段动态切换数据源的方案咨询
兄弟,你的这个数据库迁移场景我之前刚好处理过,完全懂你要的双写、动态切换读写源的需求!之前看的Baeldung文章确实是针对不同Repository绑定不同数据源的场景,对你来说不够灵活,我给你一套更贴合迁移阶段的实现思路:
核心思路拆解
我们要解决两个核心问题:迁移期间的双写同步,以及基于特性开关动态切换读写数据源,用Spring的AbstractRoutingDataSource做动态路由,再结合AOP实现双写,完全能搞定。
第一步:配置动态数据源路由
首先用Spring提供的AbstractRoutingDataSource来做数据源的动态路由,它能根据上下文标识切换不同的数据源:
- 先配置两个基础数据源(Postgres和SQL Server):
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "datasource.postgres") public DataSource postgresDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "datasource.sqlserver") public DataSource sqlServerDataSource() { return DataSourceBuilder.create().build(); } @Bean public DataSource dynamicDataSource(DataSource postgresDataSource, DataSource sqlServerDataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("POSTGRES", postgresDataSource); targetDataSources.put("SQLSERVER", sqlServerDataSource); DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource(); routingDataSource.setTargetDataSources(targetDataSources); routingDataSource.setDefaultTargetDataSource(sqlServerDataSource); // 初始默认用SQL Server return routingDataSource; } }
- 实现自定义路由数据源:
public class DynamicRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { // 从上下文Holder获取当前要使用的数据源标识 return DataSourceContextHolder.getDataSourceKey(); } }
- 定义数据源上下文Holder(用ThreadLocal保证线程安全):
public class DataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static void setDataSourceKey(String key) { contextHolder.set(key); } public static String getDataSourceKey() { return contextHolder.get(); } public static void clearDataSourceKey() { contextHolder.remove(); } }
第二步:用AOP实现双写逻辑
迁移期间需要同时写两个库,这时候AbstractRoutingDataSource只能选一个数据源,所以我们用AOP拦截所有写操作,在开关开启时同步执行两个数据源的写入:
@Aspect @Component public class DualWriteAspect { // 注入两个数据源对应的EntityManagerFactory @Autowired @Qualifier("postgresEntityManagerFactory") private EntityManagerFactory postgresEmf; @Autowired @Qualifier("sqlServerEntityManagerFactory") private EntityManagerFactory sqlServerEmf; // 双写开关,通过你的特性订阅机制实时更新 private boolean dualWriteEnabled = false; // 这里替换成你的特性开关订阅逻辑,比如监听开关变化时更新dualWriteEnabled public void onDualWriteToggleChanged(boolean enabled) { this.dualWriteEnabled = enabled; } // 拦截所有Repository的写操作(save/update/delete开头的方法) @Around("execution(* com.yourteam.yourproject.repository.*.save*(..)) || " + "execution(* com.yourteam.yourproject.repository.*.update*(..)) || " + "execution(* com.yourteam.yourproject.repository.*.delete*(..))") public Object dualWriteAdvice(ProceedingJoinPoint joinPoint) throws Throwable { // 先执行默认数据源的写操作(比如初始是SQL Server) Object result = joinPoint.proceed(); // 如果双写开关开启,同步执行Postgres的写操作 if (dualWriteEnabled) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Object[] args = joinPoint.getArgs(); // 用Postgres的EntityManager执行操作 EntityManager postgresEm = postgresEmf.createEntityManager(); try { postgresEm.getTransaction().begin(); // 获取Postgres对应的Repository实例,调用相同方法 YourEntityRepository postgresRepo = postgresEm.getRepository(YourEntity.class, YourEntityRepository.class); method.invoke(postgresRepo, args); postgresEm.getTransaction().commit(); } catch (Exception e) { postgresEm.getTransaction().rollback(); // 这里可以加日志、重试或者补偿逻辑,根据业务需求来 throw new RuntimeException("双写Postgres失败", e); } finally { postgresEm.close(); } } return result; } }
第三步:基于特性开关动态切换读数据源
同样用AOP拦截读操作,根据开关切换数据源:
@Aspect @Component public class ReadDataSourceSwitchAspect { // 读源切换开关,通过你的特性订阅机制实时更新 private boolean readFromPostgres = false; // 替换成你的开关订阅逻辑 public void onReadSourceToggleChanged(boolean enabled) { this.readFromPostgres = enabled; } // 拦截所有Repository的读操作(find开头的方法) @Before("execution(* com.yourteam.yourproject.repository.*.find*(..))") public void setReadDataSource() { if (readFromPostgres) { DataSourceContextHolder.setDataSourceKey("POSTGRES"); } else { DataSourceContextHolder.setDataSourceKey("SQLSERVER"); } } // 读完后清空上下文,避免影响后续操作 @After("execution(* com.yourteam.yourproject.repository.*.find*(..))") public void clearDataSource() { DataSourceContextHolder.clearDataSourceKey(); } }
关键注意事项
- 事务一致性:双写时如果其中一个库写入失败,要根据业务决定是否回滚另一个库,或者加补偿重试机制,避免数据不一致。
- EntityManagerFactory配置:两个数据源需要单独配置各自的
EntityManagerFactory和TransactionManager,确保事务隔离。 - 开关实时性:你的特性开关订阅要及时更新AOP里的布尔值,保证切换是实时生效的。
这个方案完全适配你的迁移流程:先开双写→切换读源到Postgres→关闭SQL Server双写,完美过渡!
备注:内容来源于stack exchange,提问作者sumek
相关产品推荐
相关产品推荐

