Redis 过期事件监听:原理与实战

1. 引言

很多场景下,我们需要在 Redis 的 Key 过期时触发一些业务逻辑,比如订单超时自动关闭、缓存失效后回源、限流窗口重置等。那么,Redis 能不能监听 Key 的过期时间呢?

答案是:可以 。Redis 从 2.8.0 版本开始支持 Keyspace Notifications(键空间通知),通过订阅 __keyevent@<db>__:expired 频道,就能在 Key 过期时收到通知。

2. 核心原理

2.1 键空间通知机制

Redis 的键空间通知(Keyspace Notifications)是一种发布/订阅(Pub/Sub)机制。当某个事件发生时(如 Key 被修改、删除、过期),Redis 会向对应的频道推送一条消息。

与 Key 过期相关的事件有两类:

  • __keyspace@<db>__:<key>:键空间通知,频道名包含具体的 Key 名。
  • __keyevent@<db>__:expired:键事件通知,频道名只包含事件类型,消息内容为过期的 Key 名。

实际开发中,我们通常订阅 __keyevent@<db>__:expired 频道,因为不需要为每个 Key 单独订阅。

2.2 过期事件的触发时机

这里有一个非常重要的细节:Redis 并不会在 Key 到达 TTL 的那一刻立即推送过期事件

Redis 删除过期 Key 有两种方式:

  • 惰性删除:当客户端访问一个已过期的 Key 时,Redis 才将其删除。
  • 定期删除:Redis 每隔一段时间(默认 100ms)随机抽取一部分设置了过期时间的 Key 进行检查,发现过期则删除。

只有 Key 真正被删除时,才会触发过期事件。因此,过期事件的通知可能会有延迟,延迟时间取决于定期删除的执行周期和 Key 的采样情况。

3. 开启过期事件监听

3.1 修改 Redis 配置

默认情况下,Redis 的键空间通知是关闭的。需要修改 redis.conf 配置文件:

conf 复制代码
notify-keyspace-events Ex

其中:

  • E:表示开启键事件通知(Keyevent)。
  • x:表示过期事件(Expired)。

如果不想修改配置文件,也可以在运行时通过命令动态开启:

bash 复制代码
redis-cli config set notify-keyspace-events Ex

注意:config set 是运行时生效,但重启后会失效。如需持久化,仍需修改配置文件。

3.2 验证是否开启

可以通过 CONFIG GET notify-keyspace-events 查看当前配置:

bash 复制代码
redis-cli config get notify-keyspace-events

输出结果中包含 Ex 即表示已开启。

4. 代码实战

4.1 使用 Redis 命令行验证

先通过命令行直观感受一下过期事件:

bash 复制代码
# 终端 1:订阅过期事件
redis-cli --csv psubscribe '__keyevent@0__:expired'

# 终端 2:设置一个 5 秒过期的 Key
redis-cli set order:1001 "pending" ex 5

等待 5 秒后,终端 1 会收到类似如下的消息:

text 复制代码
"pmessage","__keyevent@0__:expired","__keyevent@0__:expired","order:1001"

4.2 Java(Spring Boot)实现

在 Spring Boot 中,可以通过 RedisMessageListenerContainer 来监听过期事件。

首先,注册一个监听器:

java 复制代码
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;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

@Configuration
public class RedisKeyExpiredConfig {

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory connectionFactory,
            MessageListenerAdapter expiredListenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        // 订阅 db0 的过期事件
        container.addMessageListener(expiredListenerAdapter, new PatternTopic("__keyevent@0__:expired"));
        return container;
    }

    @Bean
    public MessageListenerAdapter expiredListenerAdapter(RedisKeyExpiredListener listener) {
        return new MessageListenerAdapter(listener);
    }
}

然后,实现具体的监听逻辑:

java 复制代码
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

@Component
public class RedisKeyExpiredListener implements MessageListener {

    @Override
    public void onMessage(Message message, byte[] pattern) {
        // 获取过期的 Key 名称
        String expiredKey = new String(message.getBody());
        System.out.println("Key 已过期:" + expiredKey);

        // 在这里编写业务逻辑,例如:
        // - 订单超时自动关闭
        // - 缓存失效后回源数据库
        // - 发送通知等
        if (expiredKey.startsWith("order:")) {
            handleOrderTimeout(expiredKey);
        }
    }

    private void handleOrderTimeout(String orderKey) {
        // 处理订单超时逻辑
        System.out.println("订单超时:" + orderKey);
    }
}

4.3 Python 实现

使用 redis-py 的 Pub/Sub 功能:

python 复制代码
import redis

r = redis.Redis(host="localhost", port=6379, db=0)

pubsub = r.pubsub()
pubsub.psubscribe("__keyevent@0__:expired")

print("开始监听 Redis 过期事件...")
for message in pubsub.listen():
    if message["type"] == "pmessage":
        expired_key = message["data"].decode("utf-8")
        print(f"Key 已过期: {expired_key}")
        # 在这里处理业务逻辑

5. 注意事项与局限

5.1 事件可能丢失

Redis 的 Pub/Sub 是即发即弃(fire-and-forget)模式。如果客户端在事件推送时处于断线状态,该事件就会丢失,不会重发。

对于不能容忍丢失的场景,建议改用 Redis Stream 或结合 Redisson 的延迟队列 来实现可靠的消息投递。

5.2 过期时间不精确

如前文所述,过期事件的实际触发时间会晚于 TTL 设定的时间,存在一定的延迟。对于需要精确到秒级甚至毫秒级的定时任务,不建议依赖此机制。

5.3 订阅所有 Key 的性能开销

如果 Redis 中大量 Key 频繁过期,事件通知会产生较大的网络和 CPU 开销。建议:

  • 只订阅需要的数据库(如 __keyevent@0__:expired)。
  • 通过 Key 前缀在业务侧过滤,而不是为每个业务分别订阅。

5.4 集群模式下的差异

在 Redis Cluster 模式下,Key 分散在不同的节点上,过期事件也会由对应节点推送。客户端需要订阅所有节点的过期事件,或者通过 Proxy 层统一处理,否则可能漏掉部分事件。

6. 替代方案对比

方案 实时性 可靠性 复杂度 适用场景
Keyspace Notifications 有延迟(秒级) 低(可能丢失) 非关键业务、允许延迟
Redis Stream + 消费者组 较高 高(可持久化) 需要可靠投递的业务
Redisson 延迟队列 较高 订单超时、定时任务
外部消息队列(如 RabbitMQ、Kafka) 大规模分布式系统

7. 总结

Redis 确实可以监听 Key 的过期时间,核心机制是键空间通知 。通过订阅 __keyevent@<db>__:expired 频道,我们可以在 Key 过期时收到通知并触发业务逻辑。

但需要注意,这种方案存在事件丢失触发延迟两个固有限制。对于订单超时这类对可靠性要求较高的场景,建议结合 Redis Stream 或延迟队列等方案,做到万无一失。

相关推荐
风中的小熊生气2 小时前
Spring Boot 面试知识(二):分层、IoC、异常、配置与 Redis
java·springboot
泡茶喝茶写代码2 小时前
量化数据开发实战系列(第 25 篇):基金基础接口实战:基金列表、ETF-LOF 分类、基金概况、净值数据
java·数据库·人工智能·python·mysql
shehuiyuelaiyuehao3 小时前
算法46,分治快排,排序数组
java·算法·排序算法·排序
野生技术架构师3 小时前
1200 道 JAVA 面试题(各大企业常见面试题及答案)
java·开发语言
careathers3 小时前
【数据结构】栈
java·数据结构
邪修king3 小时前
Re:Linux系统篇(二十六):文件系统(二):Ext 文件系统底层详解:从 inode、块组到软硬链接,结合 Windows 讲透文件管理本质
android·java·linux
Yyyyyy~3 小时前
【java】类和对象
java·开发语言
重生之小比特4 小时前
【Java SE】抽象类和接口
java·开发语言
Lyyaoo.4 小时前
【设计模型】策略模式
java