Redis定时任务

"Redis 定时任务"这个概念通常有两种层面的解读:

  1. 内部原理: Redis 自身是如何管理 key 的过期时间(TTL)的?它是怎么知道并在某个时间点删除数据的?

  2. 应用实现: 开发者如何利用 Redis 实现分布式的"延时队列"或"定时任务"(例如:下单 30 分钟后未支付自动取消)?

我们分别来深入讲解。


一、 Redis 内部原理:Key 的过期策略

你可能会以为当你设置 EXPIRE key 60 时,Redis 会给这个 key 启动一个倒计时器,时间一到就触发删除。但这在海量数据下是不可能的,因为 CPU 撑不住。

Redis 采用的是 惰性删除 (Lazy Deletion) + 定期删除 (Periodic Deletion) 相结合的策略。

1. 惰性删除 (Lazy Deletion)
  • 原理: Redis 不会主动去盯着 key 什么时候过期。

  • 触发时机: 当客户端去 访问 某个 key(get/set 等操作)时,Redis 会先检查:"哎,这个 key 设置了过期时间吗?过期了吗?"

    • 如果过期了:直接删除,返回 nil

    • 没过期:正常返回数据。

  • 优缺点: 极其节省 CPU,但极其浪费内存。如果大量过期的 key 再也没被访问过,它们就会一直占着内存不释放。

2. 定期删除 (Periodic Deletion)

为了解决惰性删除导致的"内存泄露"问题,Redis 有一个后台周期性任务。

  • 原理: Redis 默认每秒运行 10 次(通过 hz 参数配置)serverCron 任务。

  • 流程:

    1. 从设置了过期时间的 key 集合中,随机抽取 20 个 key。

    2. 检查这 20 个 key,删除其中已过期的。

    3. 如果过期的 key 比例超过 25%,则重复步骤 1(说明过期的很多,要多删点)。

  • 限制: 为了防止这个循环卡死主线程(Redis 是单线程的),它有一个执行时间上限(默认 25ms)。如果超时,立刻停止,等下一轮再说。

总结: Redis 的过期不是"准时"的,而是在访问时后台随机抽查时清理的。


二、 应用实现:基于 Redis 的定时任务(延时队列)

这是开发者最关心的部分。假设你要做"订单 30 分钟自动关闭",在 NestJS 或其他后端中,怎么用 Redis 实现?

方案 1:Redis ZSet (Sorted Set) ------ 最推荐、最主流

这是实现分布式延时队列的标准做法。

  • 原理: 利用 ZSet 的 Score 来存储任务的执行时间戳

  • 数据结构:

    • Key: delay_queue

    • Score: Date.now() + 30 * 60 * 1000 (未来执行的时间戳)

    • Member: Order ID (或任务的 JSON 数据)

  • 执行流程 (轮询 Loop):

    1. 消费者 (Consumer) 每秒(或几百毫秒)轮询 Redis。

    2. 执行命令 ZRANGEBYSCORE delay_queue 0 <当前时间戳> LIMIT 0 1

      • 意思是:把"截止到现在应该执行的任务"拿出来 1 个。
    3. 如果拿到了任务:

      • 原子性移除: 使用 ZREM 移除该任务(防止重复执行)。

      • 注意: 在多实例并发下,通常建议使用 Lua 脚本ZRANGEZREM 原子化,确保只有一个消费者抢到任务。

    4. 处理业务逻辑(如关闭订单)。

  • 优点: 精度高,支持海量任务,原生支持排序。

  • 缺点: 消费者需要不断轮询(Polling),空转时会增加 Redis QPS。

订单 30 分钟自动关闭的具体实现流程:

假设现在是 12:00 ,用户下了一个单 order_1001

第一步:生产者入队 (ZADD) 用户下单成功后,代码往 Redis 里写一条记录:

  • 命令: ZADD delay_queue <12:30的时间戳> "order_1001"

  • 含义: "Redis 帮我记一下,order_1001 这个单子,要在 12:30 处理。"

  • 注意:这时候数据是存在的,不是等它消失。

第二步:消费者轮询 (ZRANGEBYSCORE) 你有一个后台死循环脚本(Consumer),每秒钟问一次 Redis。

  • 12:01 问: "Redis,有没有 分数 <= 12:01 的订单?"

    • Redis:没有。
  • 12:29 问: "Redis,有没有 分数 <= 12:29 的订单?"

    • Redis:没有。(因为 order_1001 的分数是 12:30)
  • 12:30:01 问: "Redis,有没有 分数 <= 12:30:01 的订单?"

    • Redis:"有!order_1001 的分数是 12:30,它到期了!"

第三步:执行并删除 (ZREM) 消费者拿到了 order_1001

  1. 去数据库查一下这个订单支付没有?

  2. 没支付 -> 执行关单逻辑。

  3. 已支付 -> 忽略。

  4. 关键: 从 Redis 里删掉这行记录 (ZREM delay_queue "order_1001"),防止下一秒又把它取出来重复执行。

方案 2:Redis KeySpace Notifications (键空间通知) ------ 不推荐

Redis 有一个功能:当 Key 过期被删除时,发布一个 Pub/Sub 事件。

  • 原理:

    1. 开启配置 notify-keyspace-events Ex

    2. 设置 SET order_123 "data" EX 1800 (30分钟过期)。

    3. 应用订阅 __keyevent@0__:expired 频道。

    4. 当 key 过期消失时,应用收到通知,解析 key 里的 ID,去关单。

  • 为什么极其不推荐?

    1. 不可靠: Redis 的 Pub/Sub 是"发后即忘"的。如果你的服务刚好重启了,或者网络抖动了一下,这个"过期事件"就丢了,Redis 不会重发。你的订单就永远关不掉了。

    2. 不准时: 结合第一部分说的"定期删除"原理,一个 key 过期了,可能很久之后才会被 Redis 的随机算法抽中并删除,此时才会发通知。延迟可能高达数分钟。

方案 3:Redisson DelayedQueue (Java/Node 生态封装)

如果你不想手写 ZSet 的轮询逻辑,很多客户端库(如 Java 的 Redisson,或者 Node.js 的 BullMQ)封装好了。

  • 原理: 它是 ZSet + Pub/Sub 的结合体。

    • 数据存在 ZSet 里。

    • 客户端一旦有新任务加入,或者有任务即将到期,会通过 Pub/Sub 通知消费者"醒醒,干活了"。

    • 这样避免了 ZSet 方案中高频率空轮询的 CPU 浪费。

相关推荐
工藤学编程2 小时前
AI Ping 赋能:基于 GLM-4.7(免费!)+ LangChain + Redis 打造智能AI聊天助手
人工智能·redis·langchain
MoonBit月兔3 小时前
海外开发者实践分享:用 MoonBit 开发 SQLC 插件(其三)
java·开发语言·数据库·redis·rust·编程·moonbit
程序员miki3 小时前
Redis核心命令以及技术方案参考文档(分布式锁,缓存业务逻辑)
redis·分布式·python·缓存
云技纵横4 小时前
本地限流与 Redis 分布式限流的无缝切换 技术栈:Sentinel 线程池隔离 + Nginx + Kafka
redis·分布式·sentinel
云技纵横4 小时前
Redis 数据结构底层与 Hash 优于 JSON 的工程实践
数据结构·redis·哈希算法
-Xie-4 小时前
Redis(十八)——底层数据结构(三)
数据库·redis·缓存
无盐海4 小时前
Redis 集群模式Redis Cluster
数据库·redis·缓存
刘个Java4 小时前
手搓遥控器通过上云api执行航线
java·redis·spring cloud·docker
好大哥呀4 小时前
Redis解析
数据库·redis·缓存