Redis 的 key 过期事件监听和实现延迟队列

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 不包含 KE,需要显式组合,例如 KEAEx

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 核心思路

  1. 将任务作为 key 写入 Redis,并设置过期时间(TTL 即延迟时间)。
  2. 订阅过期事件频道,当 key 过期时收到通知。
  3. 在监听端解析 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

五、面试常见追问

  1. Redis 过期事件是精确的吗?
    不是。受惰性删除和定期删除影响,事件可能延迟触发。

  2. 如何保证消息不丢失?
    pub/sub 本身不保证,需配合持久化存储或定时补偿任务。

  3. Redis 集群下如何监听过期事件?
    需订阅每个 master 节点的 __keyevent@<db>__:expired,或使用代理层统一转发。

  4. 延迟队列为什么不用 Redis 的 List?
    List 不支持延迟,只能实现普通队列;延迟需要借助 ZSet 或过期事件。

  5. Redisson 延迟队列的原理?
    底层使用 ZSet 存储任务和目标时间,通过定时任务轮询到期任务并转移到目标队列。

六、总结

Redis 的 key 过期事件监听为实现延迟队列提供了一种轻量级方案,适合对可靠性要求不高的场景。其核心是开启 notify-keyspace-events Ex,订阅 __keyevent@<db>__:expired 频道,并在回调中处理业务逻辑。

但在生产环境中,由于事件丢失、延迟不精确、集群复杂等问题,更推荐使用 Redisson 的 RDelayedQueueZSet + 定时轮询 的方案。理解这些原理和权衡,才能在面试和实际开发中做出正确的技术选型。

未经允许不得转载:任鹏个人博客 » Redis 的 key 过期事件监听和实现延迟队列

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏