Spring Data Redis自动切换主备数据源

76 阅读1分钟

背景

出于高可用需求,生产中的redis有主备两个proxy,业务系统需要在其中一个proxy宕机后,自动切换到另外一个proxy。我们需要在spring-boot-starter-data-redis的基础上实现一个redis自动切换数据源的能力

实现思路

springBoot redis 的自动配置类为:org.springframework.boot.autoconfigure.data.redis.RedisProperties,可以配置单个 Redis 实例或者 Redis 集群的连接配置。根据这些配置,会生成统一的 Redis 连接工厂 RedisConnectionFactory

根据 spring-boot-autoconfigure 的自动装载源码可以知道,框架内的所有 RedisConnectionFactory 是 @ConditionalOnMissingBean 的,所以我们可以使用我们自己实现的 RedisConnectionFactory 进行替换。在我们自己的RedisConnectionFactory 中,我们可以构建主备两个redis LettuceConnectionFactory,并使用切面在其中一个获取链接异常时切换到另外一个LettuceConnectionFactory

具体实现

1、自定义ConnectionFactory

public class MultiRedisLettuceConnectionFactory
        implements InitializingBean, DisposableBean, RedisConnectionFactory, ReactiveRedisConnectionFactory {
    private final Map<String, LettuceConnectionFactory> connectionFactoryMap;

    private final AtomicBoolean useBackup = new AtomicBoolean(false);

    public MultiRedisLettuceConnectionFactory(Map<String, LettuceConnectionFactory> connectionFactoryMap) {
        this.connectionFactoryMap = connectionFactoryMap;
    }

    public void switchCurrentRedis() {
        boolean isBackup = useBackup.get();
        if (isBackup) {
            log.info("~~ Switching Redis from: backup to default !");
            useBackup.set(false);
            log.info("~~ Set Redis default !");
        } else {
            log.info("~~ Switching Redis from: default to backup !");
            useBackup.set(true);
            log.info("~~ Set Redis backup !");
        }
    }

    @Override
    public void destroy() {
        connectionFactoryMap.values().forEach(LettuceConnectionFactory::destroy);
    }

    @Override
    public void afterPropertiesSet() {
        connectionFactoryMap.values().forEach(LettuceConnectionFactory::afterPropertiesSet);
    }

    private LettuceConnectionFactory currentLettuceConnectionFactory() {
        if (useBackup.get()) {
            return connectionFactoryMap.get(CustomRedisProperties.BACKUP);
        } else {
            return connectionFactoryMap.get(CustomRedisProperties.DEFAULT);
        }

    }

    @NonNull
    @Override
    public ReactiveRedisConnection getReactiveConnection() {
        return currentLettuceConnectionFactory().getReactiveConnection();
    }

    @NonNull
    @Override
    public ReactiveRedisClusterConnection getReactiveClusterConnection() {
        return currentLettuceConnectionFactory().getReactiveClusterConnection();
    }

    @Override
    @NonNull
    public RedisConnection getConnection() {
        return currentLettuceConnectionFactory().getConnection();
    }

    @Override
    @NonNull
    public RedisClusterConnection getClusterConnection() {
        return currentLettuceConnectionFactory().getClusterConnection();
    }

    @Override
    @NonNull
    public boolean getConvertPipelineAndTxResults() {
        return currentLettuceConnectionFactory().getConvertPipelineAndTxResults();
    }

    @Override
    @NonNull
    public RedisSentinelConnection getSentinelConnection() {
        return currentLettuceConnectionFactory().getSentinelConnection();
    }

    @Override
    public DataAccessException translateExceptionIfPossible(@NonNull RuntimeException ex) {
        return currentLettuceConnectionFactory().translateExceptionIfPossible(ex);
    }
}

2、使用切面进行连接切换

@Aspect
@Component
@Slf4j
public class RedisSwitchAspect {

    @Pointcut("execution(public * enhance.redis.MultiRedisLettuceConnectionFactory.get*Connection(..))")
    public void getMultiRedisConnectionPointCut() {
    }

    @AfterThrowing(value = "getMultiRedisConnectionPointCut()", throwing = "exception")
    public void afterThrowing(JoinPoint joinPoint, Exception exception) {
        log.error("获取redisConnection异常", exception);
        CustomRedisProperties customRedisProperties = SpringContextUtil.getBean(CustomRedisProperties.class);
        if (customRedisProperties.isEnableBackup()) {
            MultiRedisLettuceConnectionFactory connectionFactory = SpringContextUtil.getBean(MultiRedisLettuceConnectionFactory.class);
            Objects.requireNonNull(connectionFactory).switchCurrentRedis();
        }
    }
}