Redis 如何实现延时队列?

阅读时长 7 分钟读完

前言

我们在前端开发过程中,经常需要用到延时处理的场景,比如在某个时间点批量发送定时任务、定时清除缓存等。针对这种场景,我们通常会选择使用延时队列。

Redis 是一个高性能的键值数据库,支持丰富的数据结构和丰富的指令,其中 zset(有序集合)就是实现延时队列的常见数据结构之一。本文将详细介绍基于 Redis 如何快速实现延时队列的具体实现方法,并提供示例代码及使用注意点进行指导。

zset 数据结构介绍

首先简要介绍下 Redis 中的有序集合 zset,在 Redis 中可直接使用 zset 来实现延时队列的存储和操作。

zset 实际上就是一个集合,其中每一个元素都被赋予一个浮点型的分数 score,根据 score 对集合中的元素进行升序或降序排列,同时每个元素都另外关联了一个字符串类型的 value。可以通过指令向 zset 添加、删除元素,或者修改元素的 score 值。zset 中的元素均为唯一的,元素的 score 值可以重复。

下面是一些常见的 Redis zset 操作指令:

  • zadd key score member [score member ...]:向 zset 添加一个或多个元素;
  • zrem key member [member ...]:从 zset 删除一个或多个元素;
  • zrange key start stop [WITHSCORES]:获取有序集合 key 中所有元素,或者指定区间的元素;
  • zrank key member:获取有序集合 key 中指定元素的排名;
  • zscore key member:获取有序集合 key 中指定元素的分数值;

延时队列实现方法

下面介绍如何在 Redis 中使用 zset 实现一个可靠的延时队列。

添加元素

可以使用 zadd 命令将任务作为 zset 中的元素加入队列,这个任务会被当做任务的“score”来插入有序集合,成为延时队列中的一个等待执行的任务。

由于浮点数排序是有精度的,我们需要确保每个任务的 score 值能够唯一确定任务,这里推荐使用时间戳作为 score 的值,如下所示:

其中,timestamp 表示任务应该被执行的时间,message 表示任务的内容。这样做的好处是,在任务处于“等待”状态时,将时间戳作为 score,保证了队列中任务的排序性,而任务出列时就可以根据时间戳获取当前需要执行的任务,比提交到后端处理任务列表中或在数据库中建立索引等其他实现方式更为高效。

注意点

当多个任务具有相同的时间戳 value 时,因为在有序集合中为任务分配的 score 值需要保证唯一性,所以我们可以使用一个唯一的 UUID 作为 value,来确保每个任务的元素在 zset 中唯一。

任务消费

当一个任务的“score”小于等于当前时间戳时,代表着该任务已经能够被消费了,我们可以使用 zrangebyscore 指令来获取当前可执行的任务,同时使用 zrem 指令将其从队列中移除。

当然,实际上这种取出来删除的方式并不是多么安全,存在很多竞态条件,比如在一个任务被执行的过程中,任务相关数据被意外删除等等。为了解决这些问题,一般需要参考 Redis 官方建议使用 RPOPLPUSH。

其具体操作流程如下:

  1. 首先备份到一个临时队列中: RPOPLPUSH delayQueue tmpQueue,保证这个任务不会被其他消费者拿走。
  2. 再使用 LREM 命令在 delayQueue队列中删除当前这个元素。LREM delayQueue 0 message。
  3. 执行任务。
  4. 如果任务处理失败(如过程异常崩溃等),将 tmpQueue 队列中的元素还原到 delayQueue 队列中:LREM tmpQueue 0 message;ZADD delayQueue timestamp message,后者将任务重新放回延时队列,等待下次重试。
  5. 如果任务处理成功,将 tmpQueue 队列中的元素删除即可:LREM tmpQueue 0 message。

示例代码

下面给出一个基于 Redis 的任务调度器示例代码。

准备工作:

  1. 搭建 Redis 环境
  2. 安装 Redis 模块: npm install redis
-- -------------------- ---- -------
-- -- ----- --
----- ----- - -----------------

-- -- -----
----- ----------- - --------------------
  ----- ------------
  ----- -----
  --- -
---

-- -- ----- ----
----------------------- --- -- -
  --------------- ----- --- ---------
---

-- ------
-------- ------------------ -------- -
  -- ----- --- ------------- ----- -
  ------------------------------ ---------- -------- ----- ---- -- -
    -- ----- -------------------- ---------
    ---- ------------------- ----------
  ---
-

-- ------- ----------------------
-------- ------------- -
  -- -------- --------------------- - -----
  -- ---------- ------------- ---------- - -------
  -- ----------- ------------- ---------- - ----- ----- - -
  --------------------------------------- -- --------------------- - ------ ------------- ----- ---- -- -
    -- ----- -------------------- ---------
    ---- -
      -- ----------- - -- -
        ----- ------- - -------
        ----- --------- - -------
        -- -- --------- ----
        ----------------------------------- ----------- ----- ---- -- -
          -- ----- -------------------- ---------
          ---- -
            -- -- ---------- --------
            ------------------------------ -- ------------- ----- ---- -- -
              -- ----- -------------------- ---------
              ---- -
                -- ----- -------------
                -------------------- -------------
              -
            ---
          -
        ---
      - ---- -
        ------------------------
      -
    -
  ---
-

-- ----------- - -- -------
----------------------------- - ----- - --- ------ --------
-- -- - ------------
------------------------ ------

总结

使用 Redis 实现延时队列具有很多的优势:

  • Redis 提供了轻量级、高性能的 zset 有序集合,开箱即用;
  • 通过延时队列实现任务调度的方式可以很好地缓解请求延时和请求压力问题;
  • 通过 Redis 的事务机制和相关指令可以实现类似锁、de-duplication 和削峰填谷这样的扩展操作;
  • 如果某个任务出现异常,我们可以通过备份队列+删除主队列的方式实现“不丢任务”的需求,可靠性极高。

不过,在使用 Redis 构建延时队列的过程中,需要注意一些问题,比如多个任务有相同的时间戳,需要使用唯一uuid作为标识;任务在消费过程中存在安全性问题,需要使用 RPOPLPUSH 等原子操作来处理。希望本文能够帮助大家对 Redis 延时队列有更好的理解和应用。

来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/648b027548841e98949648ee

纠错
反馈