SpringBoot监听Redis key失效事件


SpringBoot监听Redis key失效事件

一、问题背景

设备发送的心跳数据中的状态信息会保存在Redis缓存中,当缓存中的key超时失效时,将根据key中的设备id更新数据库中的数据,这时就需要监听Redis 的key失效事件

二、解决方案

1.开启Redis key的过期提醒

修改Redis的配置文件redis.conf,找到配置(没有就新增)notify-keyspace-event

默认为:notify-keyspace-event ""
修改为:notify-keyspace-event Ex

相关参数说明

K:keyspace事件,事件以__keyspace@__为前缀进行发布;        
E:keyevent事件,事件以__keyevent@__为前缀进行发布;        
g:一般性的,非特定类型的命令,比如del,expire,rename等;       
$:字符串特定命令;        
l:列表特定命令;        
s:集合特定命令;        
h:哈希特定命令;        
z:有序集合特定命令;        
x:过期事件,当某个键过期并删除时会产生该事件;        
e:驱逐事件,当某个键因maxmemore策略而被删除时,产生该事件;        
A:g$lshzxe的别名,因此”AKE”意味着所有事件。

2.使用Redis客户端测试

打开redis-cli客户端,监控db为0的key过期事件

config set notify-keyspace-events Ex
# __keyevent@__ db为Redis的第几个库,默认为0
PSUBSCRIBE __keyevent@0__:expired

执行命令
另打开一个客户端redis-cli,发送定时过期key

setex hello 2 world

在这里插入图片描述
观察上一个客户端,会发现接收到了过期key hello,但是无法收到hello的value
在这里插入图片描述

3.在SpringBoot中使用该特性

(1)在pom中添加Redis的依赖


    org.springframework.boot
    spring-boot-starter-data-redis

(2)定义Redis的监听配置RedisListenerConfig

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

/**
 * 

Title: RedisListenerConfig

*

Description:

* * @author Jeff * @version V1.0 * @date 2021/5/10 10:26 *

Copyright: Copyright (c) 2020 版权

*/ @Configuration public class RedisListenerConfig { @Bean RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); // 事件以__keyevent@__为前缀进行发布 container.addMessageListener(new RedisKeyExpirationListener(container), new PatternTopic("__keyevent@0__" + ":expired")); return container; } }

(3)定义监听器

实现KeyExpirationEventMessageListener接口,该接口监听所有db的过期时间keyevent@*:expired

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;

/**
 * 

Title: RedisKeyExpirationListener

*

Description:

* 该监听器监听的是所有库的key事件 * keyevent@*:expired * @author Jeff * @version V1.0 * @date 2021/5/10 10:27 *

Copyright: Copyright (c) 2020 版权

*/ @Slf4j @Component public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener { public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } @Override public void onMessage(Message message, byte[] pattern) { // 用户做自己的业务处理即可,注意message.toString()可以获取失效的key String expiredKey = message.toString(); if (expiredKey.startsWith("device:heartbeat:")) { //TODO 获取到需要处理的key,进行相关的业务处理 // 这里只能拿到的是key,不能拿到key对应的value } } }

该监听器若要使用@Autowired注入会出现注入为空的问题,解决办法参考

优化

若我们继承默认的KeyExpirationEventMessageListener,是无法动态的修改Redis的不同库的
我们可以照着KeyExpirationEventMessageListener写一个监听器,比如写一个监听数据库为6且监听删除操作的监听器

只需要修改Topic 的值即可

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.core.RedisKeyExpiredEvent;
import org.springframework.data.redis.listener.KeyspaceEventMessageListener;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.Topic;
import org.springframework.lang.Nullable;
/**
 * 

Title: KeyDeleteEventMessageListener

*

Description:

* 监听动态库的删除事件监听器 * keyevent@*:expired * @author Jeff * @version V1.0 * @date 2021/5/10 10:27 *

Copyright: Copyright (c) 2020 版权

*/ public class KeyDeleteEventMessageListener extends KeyspaceEventMessageListener implements ApplicationEventPublisherAware { @Value("${spring.redis.database}") private String database; @Nullable private ApplicationEventPublisher publisher; public KeyDeleteEventMessageListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } protected void doRegister(RedisMessageListenerContainer listenerContainer) { listenerContainer.addMessageListener(this, new PatternTopic("__keyevent@" + database + "__:del")); } protected void doHandleMessage(Message message) { this.publishEvent(new RedisKeyExpiredEvent(message.getBody())); } protected void publishEvent(RedisKeyExpiredEvent event) { if (this.publisher != null) { this.publisher.publishEvent(event); } } public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } }

然后再RedisKeyExpirationListener继承这个listener

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;

/**
 * 

Title: RedisKeyExpirationListener

*

Description:

* 该监听器监听的是所有库的key事件 * keyevent@*:expired * @author Jeff * @version V1.0 * @date 2021/5/10 10:27 *

Copyright: Copyright (c) 2020 版权

*/ @Slf4j @Component public class RedisKeyExpirationListener extends KeyDeleteEventMessageListener { public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } @Override public void onMessage(Message message, byte[] pattern) { // 用户做自己的业务处理即可,注意message.toString()可以获取失效的key String expiredKey = message.toString(); if (expiredKey.startsWith("device:heartbeat:")) { //TODO 获取到需要处理的key,进行相关的业务处理 // 这里只能拿到的是key,不能拿到key对应的value } } }