必须先配置Redis服务端启用notify-keyspace-events Ex,否则客户端订阅__keyevent@0__:expired等频道也收不到过期事件;该配置需写入redis.conf并重启生效,仅靠客户端代码无法开启。

必须先配置 Redis 服务端启用 notify-keyspace-events,否则客户端订阅任何频道都收不到过期事件。
配置 Redis 启用 Ex 事件类型
Redis 默认关闭键空间通知,仅靠客户端代码无法“打开”该功能。生产环境务必在 redis.conf 中显式设置:
notify-keyspace-events Ex
这个配置表示:只开启键事件(E)中的过期(x)通知。不推荐用 AKE 或 AKEgx 等宽泛组合,原因包括:
- 额外事件会增加 Pub/Sub 频道负载和客户端解析开销
- 某些事件(如
g类通用命令)可能触发大量非预期消息,干扰过期逻辑 - 集群环境下,每个节点都需单独配置,漏配一个就收不到对应槽位的过期事件
若临时验证,可用 redis-cli config set notify-keyspace-events Ex,但该设置在 Redis 重启后丢失,不可用于生产。
订阅 __keyevent@__:expired 频道
这是监听过期事件最直接、最常用的方式。频道名中 需替换为实际数据库编号,例如 db 0 对应:
__keyevent@0__:expired
注意以下几点:
- 不要订阅
__keyspace@0__:*并过滤 payload —— 这会收到所有操作类型(set/del/expired),需额外判断,且通配符订阅在高并发下易引发连接抖动 - Redis 不支持在频道名中使用通配符匹配多个 db,如需监听多个库,必须建立多个独立订阅连接
- Python 的
redis-py中,pubsub.subscribe()传入字符串即可;.NET 的StackExchange.Redis需用RedisChannel构造器包装,避免大小写或前缀拼接错误
消费消息时解析键名要小心 payload 格式
收到的消息结构固定为三元组:["message", "__keyevent@0__:expired", "my:order:id:123"]。关键点是:
- 第三个字段才是真实过期的键名,不是第二个(频道名)
- 如果用
__keyspace@0__:*订阅,payload 是事件类型(如expired),键名反而在频道名里,解析逻辑完全不同,极易混淆 - 某些客户端(如早期
redis-py版本)对空格或二进制 payload 处理异常,建议在消费前做isinstance(msg['data'], bytes)判断并解码 - 消息体无签名、无重试、无确认机制 —— 客户端断连期间的事件永久丢失,不能当作可靠事件总线使用
集群模式下必须逐节点订阅
Redis Cluster 不会跨节点转发 keyspace 事件。假设你有 3 个主节点,且 key user:1001 落在 node-2 上,那么只有向 node-2 发起的 SUBSCRIBE __keyevent@0__:expired 才能收到它的过期通知。
常见错误做法:
- 只连到集群入口(如 Redis Proxy 或客户端自动路由地址),期望统一收事件 —— 实际收不到
- 用单个连接执行
CLUSTER NODES后手动分发订阅,但未处理节点上下线导致的订阅中断 - 忽略从节点不产生事件这一事实,误以为在从节点订阅也能拿到数据
可行方案是:维护各主节点连接池,每个连接独占一个 PubSub 实例,并加入心跳检测与自动重连逻辑。
真正难的不是写几行订阅代码,而是理解 Redis 的事件发布时机 —— 它依赖惰性删除和定期删除两种机制,过期键可能在访问时才被清理,也可能在后台周期任务中集中处理,因此事件延迟毫秒到秒级都属正常。如果你需要亚秒级精度或强一致性回调,得换方案,比如用 Lua 脚本 + ZSET 做时间轮,或者引入外部调度器。











