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

Spring Data单Repository多数据源写入及数据库迁移阶段动态切换数据源的方案咨询

Spring Data单Repository多数据源写入及数据库迁移阶段动态切换数据源的方案咨询

兄弟,你的这个数据库迁移场景我之前刚好处理过,完全懂你要的双写、动态切换读写源的需求!之前看的Baeldung文章确实是针对不同Repository绑定不同数据源的场景,对你来说不够灵活,我给你一套更贴合迁移阶段的实现思路:

核心思路拆解

我们要解决两个核心问题:迁移期间的双写同步,以及基于特性开关动态切换读写数据源,用Spring的AbstractRoutingDataSource做动态路由,再结合AOP实现双写,完全能搞定。


第一步:配置动态数据源路由

首先用Spring提供的AbstractRoutingDataSource来做数据源的动态路由,它能根据上下文标识切换不同的数据源:

  1. 先配置两个基础数据源(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;
    }
}
  1. 实现自定义路由数据源:
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 从上下文Holder获取当前要使用的数据源标识
        return DataSourceContextHolder.getDataSourceKey();
    }
}
  1. 定义数据源上下文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();
    }
}

关键注意事项

  1. 事务一致性:双写时如果其中一个库写入失败,要根据业务决定是否回滚另一个库,或者加补偿重试机制,避免数据不一致。
  2. EntityManagerFactory配置:两个数据源需要单独配置各自的EntityManagerFactory和TransactionManager,确保事务隔离。
  3. 开关实时性:你的特性开关订阅要及时更新AOP里的布尔值,保证切换是实时生效的。

这个方案完全适配你的迁移流程:先开双写→切换读源到Postgres→关闭SQL Server双写,完美过渡!

备注:内容来源于stack exchange,提问作者sumek

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.23 10:35:33