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 或延迟队列等方案,做到万无一失。