背景
出于高可用需求,生产中的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();
}
}
}