Redis 作为高性能的内存数据库,除了常见的缓存用途外,还提供了许多高级特性。其中,key 过期事件监听(Keyspace Notifications)常被用来实现延迟队列。这也是面试中经常被问到的一个话题:如何用 Redis 实现延迟队列?本文将深入讲解其原理、配置步骤、代码实现以及生产环境中的注意事项。
一、Redis 的 key 过期事件监听机制
Redis 从 2.8.0 版本开始提供了 Keyspace Notifications 功能。当某个 key 发生特定事件(如过期、删除、修改等)时,Redis 会通过 发布/订阅(pub/sub) 机制向特定的频道发送消息。
1.1 事件类型
Redis 支持的事件分为两类:
- Keyspace 通知:以
__keyspace@<db>__:<key>为频道,消息内容是事件类型。 - Keyevent 通知:以
__keyevent@<db>__:<event>为频道,消息内容是 key 名称。
例如,当数据库 0 中的 key order:123 过期时:
- Keyspace 频道:
__keyspace@0__:order:123,消息为expired - Keyevent 频道:
__keyevent@0__:expired,消息为order:123
1.2 配置方式
默认情况下,Keyspace Notifications 是关闭的,因为开启后会带来一定的性能开销。可以通过修改 redis.conf 或使用 CONFIG SET 命令动态开启:
# 开启所有事件通知(不推荐,性能开销大)
CONFIG SET notify-keyspace-events KEA
# 只开启过期事件(推荐)
CONFIG SET notify-keyspace-events Ex
参数含义:
K:Keyspace 事件E:Keyevent 事件g:通用命令(del、expire、rename 等)x:过期事件(expired)A:等同于g$lshztdxe,即所有事件
注意:
notify-keyspace-events参数中的A不包含K和E,需要显式组合,例如KEA或Ex。
1.3 订阅过期事件
使用 Redis 客户端订阅 __keyevent@0__:expired 频道即可接收过期事件:
redis-cli
127.0.0.1:6379> PSUBSCRIBE __keyevent@0__:expired
当有 key 过期时,会收到类似消息:
1) "pmessage"
2) "__keyevent@0__:expired"
3) "__keyevent@0__:expired"
4) "order:123"
二、基于过期事件实现延迟队列
延迟队列的典型场景:订单创建后 30 分钟未支付则自动取消、定时发送通知等。利用 Redis 的过期事件,可以优雅地实现这一功能。
2.1 核心思路
- 将任务作为 key 写入 Redis,并设置过期时间(TTL 即延迟时间)。
- 订阅过期事件频道,当 key 过期时收到通知。
- 在监听端解析 key,执行对应的业务逻辑。
例如,订单延迟 30 分钟取消:
SET order:123 "cancel" EX 1800
30 分钟后,key order:123 过期,监听端收到消息并执行取消逻辑。
2.2 Java 实现示例(Spring Boot + Jedis)
@Component
public class RedisExpireListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
// 解析 key,例如 order:123
if (expiredKey.startsWith("order:")) {
String orderId = expiredKey.split(":")[1];
// 执行取消订单逻辑
cancelOrder(orderId);
}
}
private void cancelOrder(String orderId) {
System.out.println("取消订单:" + orderId);
// TODO: 调用订单服务
}
}
配置订阅:
@Configuration
public class RedisConfig {
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory factory,
RedisExpireListener listener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
// 订阅 db0 的过期事件
container.addMessageListener(listener,
new PatternTopic("__keyevent@0__:expired"));
return container;
}
}
2.3 使用 Redisson 的延迟队列
如果不想自己处理过期事件,可以直接使用 Redisson 提供的 RDelayedQueue,它底层基于 ZSet 和定时任务实现,更加可靠:
RQueue<String> queue = redisson.getQueue("orderQueue");
RDelayedQueue<String> delayedQueue = redisson.getDelayedQueue(queue);
// 30 分钟后投递到 queue
delayedQueue.offer("order:123", 30, TimeUnit.MINUTES);
// 消费端
while (true) {
String orderId = queue.take();
cancelOrder(orderId);
}
三、生产环境中的关键问题
虽然基于过期事件实现延迟队列看起来很简洁,但在生产环境中存在不少坑,面试时也常被追问。
3.1 事件丢失问题
Redis 的 pub/sub 是不持久化的,如果监听端宕机或网络抖动,消息就会丢失。此外,Redis 在 key 过期时才会发送事件,如果此时没有订阅者,事件不会被保存。
解决方案:不要只依赖过期事件,应结合定时任务补偿(如每分钟扫描一次数据库或 ZSet),保证最终一致性。
3.2 过期事件不保证及时性
Redis 的过期策略是惰性删除 + 定期删除:
- 惰性删除:访问 key 时才检查是否过期。
- 定期删除:每 100ms 随机抽查部分 key。
因此,key 过期事件可能延迟数秒甚至更久才触发。如果业务对时间精度要求高,这种方式并不合适。
3.3 集群环境下的问题
在 Redis Cluster 中,key 分散在不同节点,过期事件只在持有该 key 的节点上发布。监听端需要订阅所有节点的过期事件,增加了复杂度。
3.4 性能开销
开启 notify-keyspace-events 后,每次 key 过期都会触发一次事件发布,高频过期场景下会增加 CPU 和网络开销。建议只开启 Ex,避免使用 KEA。
3.5 重复消费
如果监听端处理失败并重试,可能导致重复消费。业务逻辑需要保证幂等性,例如通过数据库唯一约束或状态机控制。
四、更可靠的替代方案
针对上述问题,生产环境更推荐以下方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Redis ZSet + 定时轮询 | 可靠、可控 | 需自己实现轮询逻辑 |
| Redisson RDelayedQueue | 封装完善、支持集群 | 依赖 Redisson |
| RabbitMQ 延迟插件 | 高可靠、支持持久化 | 需额外维护 MQ |
| RocketMQ 延迟消息 | 原生支持、精度高 | 依赖 RocketMQ |
| 时间轮算法 | 高性能 | 实现复杂 |
ZSet 方案的核心思路:将任务 ID 和到期时间戳存入 ZSet,定时任务用 ZRANGEBYSCORE 获取到期任务并处理。
ZADD delay_queue 1735689600 order:123
# 定时任务
ZRANGEBYSCORE delay_queue 0 <current_timestamp> LIMIT 0 10
五、面试常见追问
-
Redis 过期事件是精确的吗?
不是。受惰性删除和定期删除影响,事件可能延迟触发。 -
如何保证消息不丢失?
pub/sub 本身不保证,需配合持久化存储或定时补偿任务。 -
Redis 集群下如何监听过期事件?
需订阅每个 master 节点的__keyevent@<db>__:expired,或使用代理层统一转发。 -
延迟队列为什么不用 Redis 的 List?
List 不支持延迟,只能实现普通队列;延迟需要借助 ZSet 或过期事件。 -
Redisson 延迟队列的原理?
底层使用 ZSet 存储任务和目标时间,通过定时任务轮询到期任务并转移到目标队列。
六、总结
Redis 的 key 过期事件监听为实现延迟队列提供了一种轻量级方案,适合对可靠性要求不高的场景。其核心是开启 notify-keyspace-events Ex,订阅 __keyevent@<db>__:expired 频道,并在回调中处理业务逻辑。
但在生产环境中,由于事件丢失、延迟不精确、集群复杂等问题,更推荐使用 Redisson 的 RDelayedQueue 或 ZSet + 定时轮询 的方案。理解这些原理和权衡,才能在面试和实际开发中做出正确的技术选型。
未经允许不得转载:任鹏个人博客 » Redis 的 key 过期事件监听和实现延迟队列

