不能直接用 redis pub/sub 实现可靠定时器,因其仅是信号枪而非计时器;真正可用的是 __keyevent@*:expired 事件配合 zset 轮询兜底,前者仅作轻量通知,后者为唯一可信调度源。

不能直接用 Redis Pub/Sub 实现可靠定时器,它只是个“信号枪”,不是“计时器”。真正能触发动作的只有 __keyevent@*:expired 事件,但这个事件本身不精确、不可靠,必须搭配轮询或 ZSET 等兜底机制才能用于生产。
为什么 __keyevent@0__:expired 不能当定时器用
Redis 过期机制是惰性删除 + 定期扫描混合策略,EXPIRE 只是打标记,不代表到点就删、就发事件。常见现象包括:
- 设了 5 秒 TTL,实际 8 秒后才收到
__keyevent@0__:expired - 服务刚启动订阅,中间网络断开 2 秒,这期间所有过期事件永久丢失(Pub/Sub 不存历史)
- 多个实例同时监听同一频道,一个 key 过期触发 N 次处理(没做幂等)
-
CONFIG SET notify-keyspace-events Ex在 Docker 或云 Redis(如阿里云)上默认无效,且部分托管服务禁止运行时配置
怎么让 __keyevent@*:expired 起作用
必须显式开启并验证键空间通知,否则监听永远收不到消息:
Redis 8.2.3 是一款安全优先的高性能键值存储系统。该版本紧急修复了可能引发远程代码执行(RCE)的高危漏洞(CVE-2025-62507),并解决了 HyperLogLog 及 Cuckoo Filter 等数据结构在特定场景下的崩溃问题。建议所有用户立即升级,以保障生产环境的系统稳定与数据安全。
- 确认 redis.conf 中已写入
notify-keyspace-events Ex(重启生效),或在 redis-cli 中执行CONFIG SET notify-keyspace-events Ex(仅当前实例) - 验证方式:开两个
redis-cli,一个执行PSUBSCRIBE __keyevent@0__:expired,另一个执行SET test:123 "x" EX 3;3 秒后没收到pmessage,说明配置未生效或 DB 编号不对(比如你用的是 db 2,就得订阅__keyevent@2__:expired) - Docker 启动时需加参数:
--append-only yes --notify-keyspace-events Ex;阿里云/腾讯云控制台要手动打开“键空间通知”开关
Spring Boot 监听过期事件的典型陷阱
KeyExpirationEventMessageListener 是个便利封装,但默认行为极易出错:
- 它监听所有 DB 的所有过期事件,会把 session、cache 等无关 key 全拉进来,必须手动过滤,例如:
if (new String(message.getBody()).startsWith("task:")) { ... } - 不要继承
KeyExpirationEventMessageListener后只重写onMessage却忽略message.getChannel()—— 它返回的是原始字节,需用new String(message.getBody())解析 key 名 - 监听器 Bean 必须声明为单例(
@Scope("singleton")),否则可能创建多个实例重复消费 - 推荐改用
PatternTopic("__keyevent@*:expired")手动注册,更可控
真正可用的组合方案:Pub/Sub 触发 + ZSET 轮询兜底
把 Pub/Sub 当成轻量通知,把 ZSET 当成事实来源,两者结合才能扛住生产压力:
- 任务写入:
ZADD task:schedule 1699999999 "task:abc123"(score 为时间戳) - 后台线程每秒执行:
ZRANGEBYSCORE task:schedule -inf now WITHSCORES LIMIT 0 100,取出待执行任务,再用ZREM删除 - 同时用
PUBLISH task:trigger "abc123"通知其他节点“可能有新任务了”,触发本地快速扫描 - 如果某任务在 ZSET 中存在但超时未被处理,说明节点宕机或卡住,可由其他节点通过轮询发现并接管
关键点在于:过期事件只用来“提示可能有变化”,ZSET 才是唯一可信的任务调度源。任何依赖 __keyevent@*:expired 单一通道的方案,在高负载或网络波动下都会丢任务。










