近日,在开发项目过程中,开发环境是redis单机模式,测试环境是cluster集群模式,一份代码需要同时兼容开发环境与测试环境。
配置
首先,redis配置文件中修改notify-keyspace-events Ex,默认notify-keyspace-events "",不接收任何通知,然后重启redis
以下分别是单机和集群模式下的redis部分配置
单机:
cluster集群:
nodes为集群下的各个节点
RedisConfiguration加载
在网上看了些方案,有直接改造RedisMessageListenerContainer容器来实现对于集群节点的监听,这样对于实现在单机和集群模式下都能监听产生冲突,改造开销大,所以没有去研究。
所以在配置项方面,单机和集群对于RedisTemplate和RedisMessageListenerContainer初始化保持一致:
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
//key序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
//value序列化
redisTemplate.setValueSerializer(RedisSerializer.json());
//value hashmap序列化
redisTemplate.setHashKeySerializer(RedisSerializer.string());
//key hashmap序列化
redisTemplate.setHashValueSerializer(RedisSerializer.json());
return redisTemplate;
}
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
return container;
}
}
redis单机模式下实现
网上有很多redis单机模式下实现监听的方法,这里直接上代码,创建一个类继承KeyExpirationEventMessageListener,重写onMessage方法即可。
@Slf4j
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
* 针对redis数据失效事件,进行数据处理
*
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
//获得失效的key
log.info("onMessage pattern {}, {}" ,Arrays.toString(pattern),message);
//String msg = new String(pattern,StandardCharsets.UTF_8);
String channel = new String(message.getBody());
//业务逻辑处理....
}
}
集群模式下实现监听
首先,实现每个节点对过期事件的订阅
@Slf4j
@Component
@RefreshScope
public class RedisClusterSubscriber extends RedisPubSubAdapter {
//cluster集群下,每个节点只有db0,所以这边@0即可
private static final String EXPIRED_CHANNEL = "__keyevent@0__:expired";
@Value("${spring.redis.cluster.nodes:default_value}")
private String nodes;
@Value("${spring.redis.password:default_value}")
private String password;
private static final String defaultValue = "default_value";
@Autowired
private RedisKeyExpireClusterListener redisKeyExpireClusterListener;
/**
* 启动监听,异步订阅
*/
@PostConstruct
public void subscribe() {
log.info("subscribe:{}",nodes);
//这里用是否存在nodes节点信息区分是否需要走集群订阅模式
if(!defaultValue.equals(nodes) && !defaultValue.equals(password)){
log.info("redis集群环境");
String[] nodesArr = nodes.split(",");
List<RedisURI> REDIS_URIS = Arrays.asList(
RedisURI.create("redis://"+nodes[0])
//RedisURI.create("redis://"+nodes[1])
//.....
//这里只需要创建一个节点信息的连接即可,redis会根据节点信息获取集群的拓扑结构,不过为保险起见,可以将节点信息全部加载进来。
);
//转换为char数组
char[] redisPass = password.toCharArray();
for (RedisURI redisURI : REDIS_URIS){
redisURI.setPassword(redisPass);
}
//创建客户端
RedisClusterClient clusterClient = RedisClusterClient.create(REDIS_URIS);
//尝试建立发布/订阅连接
StatefulRedisClusterPubSubConnection<String, String> pubSubConnection = null;
boolean isConnected = false;
int maxRetries = 3; //最大重试次数
int retryCount = 0;
//连接失败的话,最多尝试三次连接
while (!isConnected && retryCount<maxRetries) {
try {
pubSubConnection = clusterClient.connectPubSub();
isConnected = true;
} catch (Exception e) {
System.err.println("Failed to connect to Redis Cluster: " + e.getMessage());
retryCount++;
try {
Thread.sleep(1000); //等待1秒后重试
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
if (isConnected) {
//设置消息传播
pubSubConnection.setNodeMessagePropagation(true);
//添加监听器
pubSubConnection.addListener(redisKeyExpireClusterListener);
//选择所有节点
final PubSubAsyncNodeSelection<String, String> all = pubSubConnection.async().all();
final NodeSelectionPubSubAsyncCommands<String, String> commands = all.commands();
//订阅事件
commands.subscribe(EXPIRED_CHANNEL);
} else {
//所有重试均失败
throw new RuntimeException("Failed to connect to Redis Cluster after " + maxRetries + " retries.");
}
}else{
log.info("redis单机环境");
}
}
}
创建类继承RedisClusterPubSubAdapter适配器,重写message方法。RedisClusterPubSubAdapter用于实现Redis的发布(Pub)与订阅(Sub)功能,主要是为了在Redis集群环境中实现消息的发布和订阅,使得不同的客户端可以相互通信
@Slf4j
@Component
public class RedisKeyExpireClusterListener extends RedisClusterPubSubAdapter<String, String> {
@Override
public void message(RedisClusterNode node, String channel, String message) {
log.info("redis集群监听--->节点:{}:{},channel:{},key:{}", node.getUri().getHost(), node.getUri().getPort(), channel,message);
//业务逻辑....
}
}
问题
这里无论是单机还是集群,若是在在多服务实例下,需要考虑key的重复消费问题,需要在实现监听的逻辑中,加上redis分布式锁来规避
总结
这里将集群与单机的代码区分开,利用同一个RedisMessageListenerContainer容器,实现在两种模式下对于集群和单机redis失效key的监听处理。