登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  Redis

Redis 过期键通知怎么做自动清理:订阅、补偿扫描和幂等门禁

来源:17golang原创

时间:2026-07-08 11:13:47 441浏览 收藏

订单草稿、验证码、临时授权、上传会话这类数据,我们经常会给对应的Redis key设置TTL。真正头疼的从来不是Redis到点自动删key,而是key删掉之后,还要同步清理MySQL里的关联状态、对象存储上的临时文件,或是业务侧的各种占位记录。别把Redis过期键通知当成高可靠队列,它本质就是个提醒铃:铃响了就启动清理流程,万一铃没响,也得靠定时补偿扫描把漏下的活补完。

实践要点
  • Redis 过期键通知只保证有概率收到提醒,没法承诺每个过期key都能被消费到。
  • 业务写入临时资源的时候,要同步落一条待清理记录,TTL到期事件只是触发器,不能当作清理的唯一依据。
  • 清理任务要先走幂等门禁校验,再改业务状态,最后删除外部资源,避免重复操作把数据清错。
  • 订阅断开、Redis重启、消费者发布上线的时候都可能丢消息,补偿扫描必须按固定节奏跑才能兜底。

过期事件只负责提醒,真正清理要走任务流水线

Redis 的 keyspace notification 功能开启后,客户端可以订阅类似 __keyevent@0__:expired 的频道,收到某个key已经过期的提醒。这个机制用起来很顺手,尤其适合“到点之后触发一个后续动作”的场景,比如释放草稿锁、删除临时上传文件、注销一次性token。

但它有个很容易被忽略的边界:这个提醒不是持久化消息。订阅客户端掉线、网络抖动、Redis实例重启,都会直接让对应的过期事件丢失。生产环境更建议把它放在整个清理流水线的第一环,只负责尽早触发清理任务;后续流程还是要靠待处理表、状态字段和补偿扫描来兜底。

Redis TTL 到期后通过过期事件触发清理任务的数据生命周期示意图

打开 notify-keyspace-events 前先确认权限和通知范围

如果业务只需要关注过期事件,配置不用开得太宽。常规可以先检查当前配置:

CONFIG GET notify-keyspace-events

需要接收过期事件时,通用的配置写法是:

CONFIG SET notify-keyspace-events Ex

E 表示发布keyevent类型事件,x 表示仅推送过期相关事件。线上环境不要只临时输命令改完就完事,最好把配置写入Redis配置文件或者交给配置管理系统统一下发,否则实例重启后配置会恢复成默认值,直接收不到通知。

检查项 建议做法 容易踩的坑
事件范围 只打开过期事件需要的标记位 把所有事件标记全打开,低峰看不出问题,高峰会多出大量无用通知占用带宽
订阅库号 按业务实际使用的DB编号订阅对应频道 硬编码只监听0号库,结果测试环境和线上用的库号不同,完全收不到事件
消费者身份 用独立服务或者独立进程消费事件 把消费逻辑挂在Web请求进程里,服务发布重启的时候整段事件都错过
兜底数据 把清理上下文写入业务表的待清理状态 只依赖Redis key名带业务信息,等key过期删除后反而找不到对应的业务上下文

业务写入时就把清理上下文准备好

比较稳妥的实现模型是:创建临时资源的同时,写Redis TTL key和一条业务侧的待清理记录。比如上传会话可以写成 upload_session:{id},TTL设30分钟;MySQL里则保留 session_idobject_keyexpire_atcleanup_status。等Redis过期事件推送过来,消费者只需要从key里解析出业务ID,再回业务表读取完整的清理上下文就行。

这么做有两个明显好处。第一,Redis key删除的时候不会把对应的业务信息一起清掉;第二,即便事件没收到,直接扫描 expire_at 且 cleanup_status='pending' 的记录,照样能把遗漏的任务找回来。

清理任务要先过幂等门禁

清理这类操作最怕“看起来执行成功,其实重复跑了第二次”。比如删除对象存储文件重复调用没什么影响,但把订单状态改成已取消、把库存回滚、把积分返还这类操作,就完全不能靠运气。任务启动前先做一层门禁校验:只有 cleanup_status='pending' 的记录能被抢占,抢占成功后立刻把状态改成 processing,同时带上 cleanup_workerlocked_at

UPDATE upload_session
SET cleanup_status = 'processing',
    cleanup_worker = 'redis-expire-worker-01',
    locked_at = NOW()
WHERE session_id = ?
  AND cleanup_status = 'pending';

这条更新语句如果影响行数为1,说明当前消费者成功拿到了这条记录的处理权;如果影响行数为0,说明这条记录已经被其他消费者处理,或者状态不满足清理条件。后续的删除外部资源、更新完成状态动作,全部放在这层门禁之后执行。这个顺序比“先删文件再改状态”要稳妥很多,出问题的时候也更容易追溯补偿。

订阅漏消息时,补偿扫描要补上

Redis过期事件的优势是触发及时,补偿扫描的优势是足够可靠。两者搭配使用,才能成为可以直接放到生产环境的自动清理方案。扫描任务可以每1到5分钟跑一次,按 expire_at 分批拉取待清理记录,每批数量控制在几百条以内,避免一次性查询把数据库打满。

扫描的时候不能只看过期时间,还要专门处理卡在 processing 的旧记录。比如 locked_at 超过10分钟仍未执行完成,可以把它重新置回 pending,或者打上 retrying 状态交给下一轮扫描任务处理。这里的超时阈值要按清理动作的实际耗时来定,删除对象存储文件和调用第三方接口的等待窗口肯定不能设成一样的。

Redis 过期事件漏消息后通过补偿扫描和幂等门禁恢复清理任务的流程图

失败处理不要只写固定重试次数

配置重试次数是必要的,但不够。清理任务失败时至少要记录三类信息:失败所在的执行阶段、错误摘要、下一次允许重试的时间。比如Redis事件已经触发,业务表抢占成功,但对象存储返回404,这和数据库更新超时完全是两类问题。404大概率说明资源已经被提前删除,适合标记为完成并备注说明;数据库超时则更适合做短延迟重试。

一套简单的适配规则可以这样定:

  • 资源不存在:确认业务规则允许后标记完成,备注写清外部资源已不存在。
  • 网络超时:按30秒、2分钟、10分钟做指数递增重试。
  • 权限拒绝:停止自动重试,直接触发告警,这类问题重复跑大概率还是失败。
  • 业务状态变化:放弃清理或者转人工复核,避免把用户刚恢复的正常会话误删掉。

上线前用三组用例验证流程

这个功能很容易在开发环境测试时显得一切正常,毕竟手动设个10秒TTL,消费者一直在线,很快就能收到事件。上线前至少补三组测试用例:消费者断开期间key过期、Redis重启后key过期、清理任务执行到一半进程被杀死。所有漏下的任务都能被补偿扫描捞回来,才说明这套链路没有把可靠性完全押在订阅连接上。

另外,告警不要只盯着消费者进程是否存活。更有参考价值的是这些指标:待清理记录总数量、最老待处理记录的滞留时长、处理中超时的任务数量、连续失败的任务数量。这些指标能直观反馈清理工作是不是真的执行完了,而不是服务只是表面还在运行。

常见问题

Redis 过期键通知能替代消息队列吗?

不建议这么做。它只适合做轻量提醒,没法承诺消息100%可靠投递。需要强可靠的场景,还是应该用待处理表、Stream、专业消息队列或者任务调度系统来承载任务全生命周期状态。

过期事件没有收到,一定是 Redis 配置错了吗?

不一定。配置没开、订阅频道不匹配、消费者连接断开、实例重启都可能导致收不到。先查 notify-keyspace-events,再看消费者连接日志,最后确认后续流程有没有被补偿扫描正常接管。

补偿扫描会不会和订阅消费者同时处理同一条记录?

确实会遇到同一条记录被两边同时捕捉到的情况,所以幂等门禁必须放在所有清理动作的最前面。只有抢占状态成功的任务才能继续往下执行,不会出现重复清理问题。

清理任务失败后应该一直无限重试吗?

不要一刀切配置无限重试。网络抖动类的临时故障可以重试,权限拒绝和业务状态冲突这类场景更适合触发告警或者走人工确认。重试规则最好按失败的具体阶段拆分配置。

小结

用好Redis过期键通知,可以让临时资源的清理动作更及时;如果把它当成唯一可靠的清理链路,可靠性隐患就会藏在不稳定的订阅连接里。更稳妥的做法是把它当成触发器:业务表保存全量清理上下文,消费者先过幂等门禁再执行操作,补偿扫描定时捞取遗漏任务,失败任务进入可观测的重试和告警流程。这样即便过期事件偶尔丢失,系统也不会长期积压脏数据。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>