经过几天各种方案的对比及实验,终于完成了Springboot集成Lettuce完成Redis cluster集群的key过期监控的代码,主要参考了如下的文章及代码:
https://my.oschina.net/u/4134799/blog/3116221/print
前提条件是需要在redis的配置文件里把这个notify-keyspace-events Ex过期监控加上,我们首先要了解在Redis cluster集群中没有了多数据库的概念,只有database0,因此在监控topic的时候一定要是__keyevent@0__:expired的过期监控,否则是不会起作用的。
因为以前在使用springboot+jedis的时候是不需要考虑到主从切换时没有成功的情况的,因为它会自动每隔15秒左右就会进行自动刷新,但是切换到lettuce的时候就不一样了,它要求用户自己去定时刷新cluster节点的信息,因此需要如下配置
@Resource
private RedisProperties redisProperties;
@Bean
public LettuceConnectionFactory lettuceConnectionFactory(){
// 开启自适应集群拓扑刷新和周期拓扑刷新
ClusterTopologyRefreshOptions refreshOptions = ClusterTopologyRefreshOptions.builder()
// 开启全部自适应刷新,自适应刷新不开启,Redis集群变更时将会导致连接异常
.enableAllAdaptiveRefreshTriggers()
// 自适应刷新超时时间(默认30秒)
.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30))
// 开启周期刷新,默认关闭,开启后时间默认为60秒
.enablePeriodicRefresh(Duration.ofSeconds(10))
.build();
ClientOptions clientOptions = ClusterClientOptions.builder()
.topologyRefreshOptions(refreshOptions)
.build();
LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
.poolConfig(genericObjectPoolConfig(redisProperties))
//.readFrom(ReadFrom.MASTER_PREFERRED)
.clientOptions(clientOptions)
// 默认RedisURI.DEFAULT_TIMEOUT 60
.commandTimeout(redisProperties.getTimeout())
.build();
List<String> clusterNodes = redisProperties.getCluster().getNodes();
Set<RedisNode> nodes = new HashSet();
clusterNodes.forEach(address -> {
String[] ipPort = address.split(":");
nodes.add(new RedisNode(ipPort[0].trim(), Integer.valueOf(ipPort[1])));
});
RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration();
clusterConfiguration.setClusterNodes(nodes);
//clusterConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
clusterConfiguration.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(clusterConfiguration, clientConfig);
// 是否允许多个线程操作共用同一个缓存连接,默认true,false时每个操作都将开辟新的连接
// lettuceConnectionFactory.setShareNativeConnection(false);
// 重置底层共享连接, 在接下来的访问时初始化
// lettuceConnectionFactory.resetConnection();
return lettuceConnectionFactory;
}
@Bean
public GenericObjectPoolConfig<?> genericObjectPoolConfig(RedisProperties redisProperties) {
GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
RedisProperties.Pool properties = redisProperties.getLettuce().getPool();
config.setMaxTotal(properties.getMaxActive());
config.setMaxIdle(properties.getMaxIdle());
config.setMinIdle(properties.getMinIdle());
if (properties.getMaxWait() != null) {
config.setMaxWaitMillis(properties.getMaxWait().toMillis());
}
return config;
}
lettuce是springboot默认的引入,通过上面的配置信息,在集群节点发生改变时,它会去主动的定时刷新集群中redis的状态,接下来我们就需要一个定时任务来获取Redis集群中的所有master节点,从而做到master节点中的key值过期时可以获取到过期的key,为什么只是监听master节点是因为slave节点只是做个备份,不发生任何和变化操作相关的操作,看下定时监听代码
@Component
public class RedisMessageRefreshClusterNodeFactory {
@Resource
private RedisConnectionFactory redisConnectionFactory;
@Resource
private RedisExpiredMessageListener messageListener;
@Resource
private ListenerSetting listenerSetting;
/**
* 用于存储已经启动监听的master节点信息
*/
ConcurrentHashMap masterNodeMap = new ConcurrentHashMap();
@Scheduled(cron = "0/5 * * * * ?")
public void refreshClusterNode() {
RedisClusterConnection clusterConnection = redisConnectionFactory.getClusterConnection();
if (Optional.ofNullable(clusterConnection).isPresent()) {
/*
结束了旧的master节点后,旧的节点为master节点的状态不会改变,
只有重启旧的master节点后变为了slave节点获取到的状态才会发生变化
*/
Iterable<RedisClusterNode> redisClusterNodes = clusterConnection.clusterGetNodes();
// 用于获取当前的master节点信息
HashMap currentMasterNodeMap = new HashMap();
redisClusterNodes.forEach(redisClusterNode -> {
if (redisClusterNode.isMaster()) {
String clusterNodeName = "clusterNodeName" + redisClusterNode.hashCode();
String hostPort = redisClusterNode.getHost() + ":" + redisClusterNode.getPort();
currentMasterNodeMap.put(clusterNodeName, hostPort);
// 不进行重复创建监听
if (masterNodeMap.containsKey(clusterNodeName)) {
return;
}
masterNodeMap.put(clusterNodeName, hostPort);
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisClusterNode.getHost(), redisClusterNode.getPort());
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration);
connectionFactory.afterPropertiesSet();
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 设置监听使用的线程池
container.setTaskExecutor(ExecutorUtil.executorService);
// 设置监听的Topic
ChannelTopic channelTopic = new ChannelTopic(listenerSetting.getMerchantTopicExpired());
// 设置监听器
container.addMessageListener(messageListener, channelTopic);
// 必须调用,否则空指针异常
container.afterPropertiesSet();
// 启动监控
container.start();
}
});
// 比较2个map中的值,去除不存在的值
masterNodeMap.forEach((key, value) -> {
if (!currentMasterNodeMap.containsKey(key)) {
masterNodeMap.remove(key);
}
});
}
}
}
需要注意的是要启动定时任务,必须在启动类上面添加@EnableScheduling注解,那么剩下的listener类就如下
@Component
@Slf4j
public class RedisExpiredMessageListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] bytes) {
// 过期的key
String expiredKey = new String(message.getBody());
//对监听消息进行处理
if (expiredKey.contains("")) {
}
}
}