当前位置: 首页 > news >正文

Redis 监听过期Key

Redis 监听过期Key

    • Redis 键空间通知
      • 事件驱动
        • 文件事件
        • 时间事件
    • 打开方式
      • 动态配置(临时开,重启失效)
      • 静态配置(重启redis-server生效)
      • 事件类型表
    • 事件是怎么样发送的?
    • 实测
    • 使用场景
      • 缺点
    • 在SpringBoot应用
      • 依赖
      • KeyspaceEventMessageListener
        • 样例
      • KeyExpirationEventMessageListener
        • 样例
          • @EventListener
          • 继承并重写

Redis 键空间通知

实时监控Redis键和值的变化

注意:Redis Pub/Sub 是即发即弃的;也就是说,如果 Pub/Sub 客户端断开连接,然后稍后重新连接,则客户端断开连接期间传递的所有事件都将丢失。

事件驱动

Redis 定义了两种事件类型,一种是文件事件,一种是时间事件。不同了事件类型通过不同的处理去处理。

文件事件

可以理解为是IO操作:建立连接,读请求,写响应。

时间事件

定时执行:过期key,AOF等。

打开方式

redis 默认是不发送任意事件通知的。

动态配置(临时开,重启失效)

CONFIG SET notify-keyspace-events AKE

静态配置(重启redis-server生效)

redis.conf中找到 notify-keyspace-events ""修改为 notify-keyspace-events "AKE"则表示开启所有事件通知。

事件类型表

字母所属维度含义说明典型命令/场景
K通道维度Keyspace 事件,频道名格式 __keyspace@<db>__:<key>监听“某个键发生过什么”
E通道维度Keyevent 事件,频道名格式 __keyevent@<db>__:<cmd>监听“某个命令影响了哪些键”
g命令维度通用命令(与类型无关)DEL、EXPIRE、RENAME、MOVE、SWAPDB 等
$命令维度字符串专属命令SET、MSET、APPEND、INCR、DECR …
l命令维度列表专属命令LPUSH、RPOP、LTRIM …
s命令维度集合专属命令SADD、SPOP、SINTER …
h命令维度哈希专属命令HSET、HDEL、HINCRBY …
z命令维度有序集合专属命令ZADD、ZREM、ZINCRBY …
t命令维度Stream 专属命令XADD、XDEL、XTRIM …
d命令维度模块键类型事件(Module key)自定义模块产生的键
x生命周期过期事件(expired)键因 TTL 到达被删除
e生命周期淘汰事件(evicted)键因 maxmemory 策略被逐出
m生命周期未命中事件(miss)访问了一个不存在的键
n生命周期新建事件(new)第一次向一个键写入值
A快捷别名相当于 g$lshztxed 的集合(不含 m、n)想“一键全开”时常用

事件是怎么样发送的?

本质:Redis 发布/订阅

执行命令/键修改时 -> 触发对应回调-> 调用 publish 向固定的频道(__keyspace@<db>__:<key>||__keyevent@<db>__:<event>)发送消息事件。

口诀:“双下划线,keyspace / keyevent 二选一;@库号,冒号后跟事件名;过期找 expired,淘汰找 evicted。”

维度频道格式示例(库 0)说明
keyspace__keyspace@<db>__:<key>__keyspace@0__:mylock告诉你“哪个键发生了事”
keyevent__keyevent@<db>__:<event>__keyevent@0__:expired告诉你“什么事发生了

不同命令产生的事件:官方文档 https://redis.io/docs/latest/develop/pubsub/keyspace-notifications/#events-generated-by-different-commands

**重要提示:**所有命令仅在目标键真正被修改时才会生成事件。例如, DELETE 删除一个不存在的元素并不会真正改变该键的值,因此不会生成任何事件。

监听过期key的时候只需要关注:expired

补充:每次根据**maxmemory**策略从数据集中逐出一个键以释放内存时(内存不够,驱逐未使用过的key): <font style="color:rgb(17, 24, 39);">evicted

实测

监听:**psubscribe __key*__:***

127.0.0.1:6379> psubscribe __key*__:*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"     # 通过模式频道监听
2) "__key*__:*"     # 监听的模式平道
3) (integer) 1      # 当前客户端监听的频道

另外启动一个客户端

127.0.0.1:6379> set hello world;  # 创建hello这个键
OK
127.0.0.1:6379> expire hello 10   # 给hello这个键设置一个过期时间
(integer) 1
127.0.0.1:6379>

观察监听的哪个客户端

1) "pmessage"         				# 发送消息
2) "__key*__:*"								# 发送消息的模式频道
3) "__keyspace@0__:hello"			# 发送消息的实际频道
4) "set"											# 发送消息的内容   这里表示对hello这个key进行了set操作
1) "pmessage"
2) "__key*__:*"
3) "__keyevent@0__:set"
4) "hello"										# 这里表示通过set命名操作了hello这个key
1) "pmessage"
2) "__key*__:*"
3) "__keyspace@0__:hello"
4) "expire"
1) "pmessage"
2) "__key*__:*"
3) "__keyevent@0__:expire"		# 这里表示通过expire对hello设置了一个过期时间
4) "hello"
1) "pmessage"									
2) "__key*__:*"
3) "__keyspace@0__:hello"			
4) "expired"
1) "pmessage"
2) "__key*__:*"
3) "__keyevent@0__:expired"		# hello 这个key过期后会触发发送事件 
4) "hello"

了解以上内容后我们便可以监听事件来实现业务中的一些功能了。但是注意redis 发布/订阅是不可靠的。他不能保证一定会你发消息。如果要用一定要添加补偿机制。

使用场景

  1. 订单超市未支付-----订单的有效期为ttl,ttl到期后处理监听事件,来做关闭订单,解锁库存
  2. 配置刷新-----读取配置后给配置一个ttl监听该key,如果ttl则重新拉取配置
  3. 限流窗口----重新计数–可以做一个简单的限流-> 当某一个第一次接受到请求的时候往redis添加一个key并将value给1,并设置ttl为60,然后再接受同样请求的时候对这个key的value+1,如果该key的value值大于某个界限的时候可以在程序中直接拦截掉->如果该key过期了,可以通过监听重新设置该key的value为1。如此循环。

缺点

  1. 消息丢失:redis pub/sub 是无持久化。
  2. 消息重复消费:多节点下多个会同时收到过期key的消息
  3. 大key同时过期:节点压力会剧增
  4. 只发一次:过期后立刻就发送消息,当时没收到以后也不会收到。

在SpringBoot应用

Java:21
SpringBoot:3.5.5
Redis-client:Spring-data-redis 3.5.5+Lettuce-6.x

依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

KeyspaceEventMessageListener

这是spring-data-redis 内置的一个 抽象类KeyspaceEventMessageListener。只需要继承该类,并重写他的doHandleMessage方法即可监听__keyevent@*

public abstract class KeyspaceEventMessageListener implements MessageListener, InitializingBean, DisposableBean {// 监听的渠道private static final Topic TOPIC_ALL_KEYEVENTS = new PatternTopic("__keyevent@*");public void init() {RedisConnectionFactory connectionFactory = listenerContainer.getConnectionFactory();if (StringUtils.hasText(keyspaceNotificationsConfigParameter) && connectionFactory != null) {try (RedisConnection connection = connectionFactory.getConnection()) {RedisServerCommands commands = connection.serverCommands();Properties config = commands.getConfig("notify-keyspace-events");// 这个是配置是否包含notify-keyspace-events 用于为连接动态打开键空间通知if (!StringUtils.hasText(config.getProperty("notify-keyspace-events"))) {commands.setConfig("notify-keyspace-events", keyspaceNotificationsConfigParameter);}}}// 1.注册doRegister(listenerContainer);}/*** Register instance within the container.** @param container never {@literal null}.*/protected void doRegister(RedisMessageListenerContainer container) {// 2.向listener容器中添加该渠道表示监听渠道listenerContainer.addMessageListener(this, TOPIC_ALL_KEYEVENTS);}@Overridepublic void onMessage(Message message, @Nullable byte[] pattern) {if (ObjectUtils.isEmpty(message.getChannel()) || ObjectUtils.isEmpty(message.getBody())) {return;}doHandleMessage(message);}/*** Handle the actual message* 实际消费消息的地方* @param message never {@literal null}.*/protected abstract void doHandleMessage(Message message);
}
样例
// 注意要讲该对象添加为Bean对象
@Slf4j
@Component 
public class ConsumeKeyspaceEventMessageListenerImpl extends KeyspaceEventMessageListener {/*** Creates new {@link KeyspaceEventMessageListener}.** @param listenerContainer must not be {@literal null}.*/public ConsumeKeyspaceEventMessageListenerImpl(RedisMessageListenerContainer listenerContainer) {super(listenerContainer);}@Overrideprotected void doHandleMessage(Message message) {log.info("channel:{},body:{}", new String(message.getChannel()), new String(message.getBody()));}
}

KeyExpirationEventMessageListener

键过期的专用消息监听器

它继承了KeyspaceEventMessageListener只关注__keyevent@*__:expired

它默认给我们实现的操作是:发送一个event(RedisKeyExpiredEvent)给Spring容器。让Spring的事件机制再去处理。

public class KeyExpirationEventMessageListener extends KeyspaceEventMessageListenerimplements ApplicationEventPublisherAware {// 只关注该频道private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired");private @Nullable ApplicationEventPublisher publisher;@Overrideprotected void doRegister(RedisMessageListenerContainer listenerContainer) {// 1 注册listenerContainer.addMessageListener(this, KEYEVENT_EXPIRED_TOPIC);}// 默认实现@Overrideprotected void doHandleMessage(Message message) {// // 2.  将内容包装RedisKeyExpiredEvent为发送给Spring容器publishEvent(new RedisKeyExpiredEvent(message.getBody()));}/*** Publish the event in case an {@link ApplicationEventPublisher} is set.** @param event can be {@literal null}.*/protected void publishEvent(RedisKeyExpiredEvent event) {if (publisher != null) {this.publisher.publishEvent(event);}}}

如果我们自己想对键过期做专门的处理,有两种方式

  1. 直接通过@EventListener处理RedisKeyExpiredEvent该事件即可。
  2. 继续KeyExpirationEventMessageListener 重写doHandleMessage即可。
样例
@EventListener

要保证容器中存在KeyExpirationEventMessageListener的组件才会在key过期的时候发送RedisKeyExpiredEvent

@Bean
public KeyExpirationEventMessageListener keyExpirationEventMessageListener(RedisMessageListenerContainer redisMessageListenerContainer){return new KeyExpirationEventMessageListener(redisMessageListenerContainer);
}@Bean
public RedisKeyExpiredEventListener redisKeyExpiredEventListener() {return new RedisKeyExpiredEventListener();
}@Slf4j
public class RedisKeyExpiredEventListener {/*** 通过Spring的EventListener处理过期key** @param event*/@EventListenerpublic void onMessage(RedisKeyExpiredEvent<byte[]> event) {log.info("expired key: {}", new String(event.getId()));log.info("timestamp: {}", event.getTimestamp());}
}
继承并重写

需要重写doHandleMessage来实现自定义逻辑

@Bean
public ConsumeKeyExpirationEventMessageListener consumeKeyExpirationEventMessageListener(RedisMessageListenerContainer redisMessageListenerContainer) {return new ConsumeKeyExpirationEventMessageListener(redisMessageListenerContainer);
}
@Slf4j
public class ConsumeKeyExpirationEventMessageListener extends KeyExpirationEventMessageListener {/*** Creates new {@link MessageListener} for {@code __keyevent@*__:expired} messages.** @param listenerContainer must not be {@literal null}.*/public ConsumeKeyExpirationEventMessageListener(RedisMessageListenerContainer listenerContainer) {super(listenerContainer);}/*** 通过继承并重写doHandleMessage来达到监听某个key过期* @param message never {@literal null}.*/@Overrideprotected void doHandleMessage(Message message) {// 保留发送RedisKeyExpiredEvent事件,以便其他地方使用super.doHandleMessage(message);log.info("expired key: {}", new String(message.getBody()));log.info("channel: {}", new String(message.getChannel()));}
}
http://www.dtcms.com/a/399067.html

相关文章:

  • 随身WiFi二合一设备开发深度解析:Wi-Fi 6模块集成与三网切换算法优化
  • 国内做外单的网站有哪些200元网站建设
  • 口碑好的江苏网站建设wix做的网站在国内访问不
  • 学习目标:HTML快捷键
  • RoadCLIP 笔记 针对自动驾驶优化的 CLIP 变体 vlm
  • 一文可视化分析2025年8月arXiv机器学习前沿热点
  • 网站闭关保护怎么做找人做的网站怎么看ftp
  • 高性能内存池(一)----项目整体架构设计
  • 笔记(C++篇)—— Day 10
  • Express使用教程(三 - 集成mysql)
  • Universal bovine identification via depth data and deep metric learning 阅读笔记
  • MySQL中limit 1的用法
  • 西门子840d sl 学习笔记 持续更新中。。。
  • 大良网站建设公司网站空间推荐
  • 数学教学网站开发商务网站建设PDF下载
  • 如何查看vmware主机支持的EVC(Enhanced vMotion Compatibility)模式
  • Spring List接口类型的自动注入
  • DeepSeek发布V3.1-Terminus版本,模型性能与Agent能力全面升级
  • 中国制造网外贸站宣传软文模板
  • C++?智能指针!!!
  • Python环境终极排错指南:彻底解决pip install失败问题
  • 【JAVA】Java抽象类与接口详解:特性与实战运用(超详细)
  • Shell 脚本知识体系
  • 怎么创立网站官网开发多少钱一个
  • RK3568+MCU实时机器人解决方案
  • (队列)Leetcode239 滑动窗口的最大值
  • 企业网站的建设意义付费阅读网站代码
  • 青岛网站上排名美容医疗 网站建设
  • 网站实名认证必须做么做网站的html框架
  • 采用libreoffice将word、excel等文件转换为pdf格式