redis过期事件通知不精确且不可靠,因依赖惰性与定期删除混合策略,存在延迟、消息丢失、重复触发等问题,不能单独用于生产级定时任务,需配合zset轮询等兜底机制。

Redis 的 __keyevent@*__:expired 事件通知不是“定时器”,它只在 key 真正过期时才触发,而 Redis 过期本身不精确——你设了 5 秒,实际可能延迟几十毫秒甚至几秒才发通知,尤其在高负载或低频访问场景下。
为什么不能直接用 SET + EXPIRE + __keyevent@0__:expired 做任务调度
因为 Redis 的过期是惰性+定期删除混合策略,EXPIRE 只是“标记过期时间”,不代表到点就删、就发事件。常见现象包括:
- 你
SET task:123 "run" EX 10,但 12 秒后才收到__keyevent@0__:expired消息 - 服务刚启动时订阅了频道,但中间网络抖动断连 3 秒,这期间发生的过期事件永远丢失(Redis Pub/Sub 不保证消息可达)
- 多个实例同时订阅同一频道,一个过期 key 触发 N 次处理(没做幂等校验)
-
CONFIG SET notify-keyspace-events Ex没生效,或者配成了KEA却误以为只监听 expired,结果收到一堆del、set干扰事件
必须显式开启并验证 notify-keyspace-events 配置
运行时配置只对当前实例生效,重启即失效;生产环境务必写入 redis.conf。验证是否生效,别只看 CONFIG GET notify-keyspace-events 返回 Ex,要实测:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 开两个
redis-cli:一个执行PSUBSCRIBE __keyevent@0__:expired - 另一个执行
SET test_trigger "x" EX 3 - 3 秒后第一个窗口是否收到
pmessage?没收到就说明配置未生效或数据库编号不对(比如你用的是 db 2,就得订阅__keyevent@2__:expired) - 注意:Docker 或云 Redis(如阿里云、腾讯云)默认关闭该功能,且部分托管服务禁止
CONFIG SET,需在控制台开启“键空间通知”开关
Spring Boot 中监听过期事件的坑点
Spring Data Redis 提供的 KeyExpirationEventMessageListener 是个便利封装,但它默认监听所有 DB 的所有过期事件,且不做 key 名过滤。真实业务中你通常只关心 task: 或 order: 开头的 key,否则会把缓存、Session 等无关过期也拉进来处理:
- 不要直接继承
KeyExpirationEventMessageListener后重写onMessage却忽略message.getChannel(),它返回的是原始字节,需用new String(message.getBody())解析 key 名 - 推荐手动注册
PatternTopic("__keyevent@*__:expired"),并在回调里先判断if (key.startsWith("task:")) { ... } - 监听器 Bean 必须是单例(
@Scope("singleton")),否则容器可能创建多个实例,导致重复消费 - 如果用了 Redis 集群,
__keyevent@*__:expired只在 key 所在 slot 的节点上触发,订阅必须覆盖所有 master 节点(Spring 默认只连一个节点,需配置LettuceClientConfigurationBuilderCustomizer或改用集群模式监听)
过期事件 ≠ 定时任务,必须补足可靠性短板
单纯依赖 expired 事件无法构建生产级定时任务,它只是“尽力通知”。真正可用的方案得叠加补偿机制:
- 写任务时,除了
SET task:123 "payload" EX 60,还要往zset里加一条:ZADD task_delay_queue 1748238180 task:123(时间戳为绝对秒数) - 另起一个低频轮询线程(比如每 30 秒扫一次
ZRANGEBYSCORE task_delay_queue -inf now),捞出已到时间的任务,用 Lua 脚本原子地ZREM + 处理,并确保只被一个实例抢到 - 事件监听器收到
expired后,仅作快速路径处理;若失败或未收到,则靠 ZSET 轮询兜底——两者是“快慢双通道”,不是二选一 - 所有任务处理逻辑必须可重入:同一个
task:123可能被事件触发一次、又被 ZSET 扫描触发一次,状态更新要用SETNX或 Lua 校验再执行
最易被忽略的一点:Redis 本身不保证过期事件 100% 发出。如果你的任务要求“严格准时”,比如金融结算倒计时,别碰这个方案——换 Quartz + 数据库时间轮,或者用 Redis Stream + consumer group 做带 ACK 的延迟队列。










