Redis 监听过期Key - 指南

news/2025/9/25 17:38:52/文章来源:https://www.cnblogs.com/tlnshuju/p/19111654

Redis 监听过期Key - 指南

2025-09-25 17:34  tlnshuju  阅读(0)  评论(0)    收藏  举报

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);
}
@Override
public 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);
}
@Override
protected 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 KeyspaceEventMessageListener
implements ApplicationEventPublisherAware {
// 只关注该频道
private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired");
private @Nullable ApplicationEventPublisher publisher;
@Override
protected void doRegister(RedisMessageListenerContainer listenerContainer) {
// 1 注册
listenerContainer.addMessageListener(this, KEYEVENT_EXPIRED_TOPIC);
}
// 默认实现
@Override
protected 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
*/
@EventListener
public 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}.
*/
@Override
protected 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.mzph.cn/news/917315.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

11_Reactor网络模型

一、Reactor模型基本原理 Reactor模型是一种基于事件驱动的设计模式,特别适合处理高并发的I/O密集型应用。Reactor模型的核心思想很简单,但又很巧妙,它围绕着"事件"展开。不同于传统模型中线程主动等待I/…

花卉网站建设策划方案做的比较好看的网站

一、引入方式 JavaScript 程序不能独立运行&#xff0c;它需要被嵌入 HTML 中&#xff0c;然后浏览器才能执行 JavaScript 代码。通过 script 标签将 JavaScript 代码引入到 HTML 中 1️⃣内部 通过 script 标签包裹 JavaScript 代码&#xff08;一般就写在</script>的…

「LNOI2022」盒

🦁🦁🦁🦁🦁🦁🦁🦁🦁我要把你开盒挂网上。我们定义一波: \(s_i = \sum_{j = 1}^{i} a_j\) 那我们确定 \(b\) 后,答案是好算的,我们用 \(z_i\) 表示 \(b\) 的前缀和,就有: \[ans_b = \sum_{i = 1…

【文摘随笔】从业开发工作五年后,再读短篇《孔乙己》——年少不懂孔乙己,长大已成孔乙己

再读《孔乙己》对我而言有太多理由,*鲁迅*、*周树人* 作为网络 meme 已经过去了 7 年,但我的起点是人们越发的使用“脱不掉的长衫”和“孔乙己”来讽刺一个角色、一种现象。 中学时全文背诵的课文竟是一点没有印象,…

为什么我选择了 PSM 敏捷认证?

PSM考试优惠码:4DBC2DE748最近身边不少朋友问我: “敏捷证书这么多,你为什么选了 PSM(Professional Scrum Master)?” 今天就来聊聊我自己的感受,或许对你也有参考价值~ 💡 PSM 解决了我职业发展的几个痛点 …

宫廷计有哪些网站开发的页面跳转的方法

简介&#xff1a; Loki是受Prometheus启发的水平可扩展、高可用、多租户日志聚合系统。用户既可以将Loki告警直接接入SLS开放告警&#xff0c;也可以先将Loki接入Grafana或Alert Manager&#xff0c;再借助Grafana或Alert Manager实现Loki间接接入SLS开放告警。 直接接入 您可…

app开发与网站开发的区别建设工程自学网站

jenkins复制作业您可能知道&#xff0c;Jenkins是高度可配置的CI服务器。 我们可以设置不同的自定义构建过程。 我将分享一些我用来设置詹金斯工作层次的方法。 这是用例&#xff1a; 我们有一个主要的入口工作被调用以启动整个构建过程。 这项工作可以有一个到多个子工作。 …

Hive SQL - INSERT

Hive SQL - INSERT INSERT INTO TABLE zzh_test VALUES (1, 1,1. AAA), (2, 2.2, BBB);INSERT OVERWRITE TABLE zzh_test SELECT * FROM zzh_test; INSERT INTO zzh_test VALUES (1, 1,1. AAA), (2, 2.2, BBB);INSERT …

石家庄房和城乡建设部网站网站建设属于设备吗

打算刷一遍nssweb题&#xff08;任重道远&#xff09; 前面很简单 都是签到题 这里主要记录一下没想到的题目 [GDOUCTF 2023]hate eat snake 这里 是对js的处理 有弹窗 说明可能存在 alert 我们去看看js 这里进行了判断 如果 getScore>-0x1e9* 我们结合上面 我觉得是6…

建站点wordpress 全局字段

文章目录 一、序二、机械硬盘和固态硬盘的物理结构与工作原理2.1 机械硬盘2.11 基本结构2.12 工作原理 2.2 固态硬盘2.21 基本结构2.22 工作原理 三、机械硬盘和固态硬盘的垃圾回收机制3.1 机械硬盘GC3.2 固态硬盘GC3.3 TRIM指令开启和关闭 四、做好数据备份 一、序 周末电脑突…

网站开发如何设置视频教程设计美观网站有哪些

编者按 伏羲&#xff08;Fuxi&#xff09;是十年前最初创立飞天平台时的三大服务之一&#xff08;分布式存储 Pangu&#xff0c;分布式计算 MaxCompute&#xff0c;分布式调度 Fuxi&#xff09;&#xff0c;当时的设计初衷是为了解决大规模分布式资源的调度问题&#xff08;本…

大庆建设银行网站首页网站怎么容易被百度收录

数据持久化到Flash 文章目录 数据持久化到Flash1、Preferences库介绍2、软件准备3、硬件准备4、代码实现4.1 初始化NVS Flash4.2 读写Key/Value对4.3 保存/读取网络凭据4.4 复位后记住最后的 GPIO 状态在本文中,我们将介绍如何使用 Preferences库将数据存储到 ESP32 的Flash中…

编写msyql8.0.21 数据库批量备份脚本

编写msyql8.0.21 数据库批量备份脚本一:编写mysql数据库备份my.cnf文件二、编写数据库导出脚本czywxt_nacos.bat@echo off chcp 65001 > nul title MySQL Backup for czywxt_nacos setlocal disabledelayedexpansi…

完整教程:基础算法---【差分】

完整教程:基础算法---【差分】pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco&quo…

Android 源码中如何生成一个platform JKS 文件?

首先我们需要在源代码环境中将 build/target/product/security/ 文件夹 copy 到本地。 下边的操作需要在 ubuntu 或者 mac 下。重要安全提醒:platform 密钥是系统级私钥,拥有它就能签出系统权限应用。不要把它放到公…

后端面试八股(go 方向)

go 后端面试准备 一、Go语言相关 1、Go里有哪些数据结构是并发安全的?int类型是并发安全的吗?sync 包中的类型sync.Mutex 和 sync.RWMutex:互斥锁,通过加锁机制保证临界区安全 sync.WaitGroup:用于等待一组 gorou…

ArcGIS 不重叠且无缝的拓扑检查和修改

ArcGIS 不重叠且无缝的拓扑检查和修改创建拓扑: 新建数据库→新建dataset→导入要素 dataset右键新建topo 设置容差和规则 拓扑容差: 0.001 默认标准 0.00001 清查标准 注意:容差为分辨率两倍 拓扑规则: 1.不能重…

C++设计模式之创建型模式:工厂方法模式(Factory Method) - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

【铸网-2025】线下赛 web 详细题解

<?php show_source(index.php); class MGkk8 {public $a;public $b;public function rpl2(){echo(MGrp12;);$b = $this->b;if ($this->a == "RPG") {echo(ifyes;);($b->a)($b->b."&quo…

2025/9/25

A 用时:1h 预期:100pts 实际:100pts 发现有两种做法,可以直接模拟,递推,复杂度分别为 \(O(n^2)\),\(O(n^2\log n)\),而递推可以用 bitset 压一下。 考虑根号分治复杂度为 \(O(B \times n+\frac{n^2\times log …