基于 Redis Stream 的消息队列

本文介绍了Redis Stream作为消息队列的实现,强调其持久化和主备复制功能,对比了与发布订阅的区别。内容包括Stream的结构、消息队列相关命令、消费者组操作及如何使用Stream进行消息生产和消费,详细讲解了XADD、XGROUP、XREADGROUP、XACK等关键命令的使用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基于 Redis Stream 的消息队列

参阅

Redis Stream 是 Redis 5.0 版本新增加的数据结构。

Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。

简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。

而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

Redis Stream 的结构如下所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容:

在这里插入图片描述

每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用 xadd 指令追加消息时自动创建。

上图解析:

  • Consumer Group :消费组,使用 XGROUP CREATE 命令创建,一个消费组有多个消费者(Consumer)。
  • last_delivered_id :游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
  • pending_ids :消费者(Consumer)的状态变量,作用是维护消费者的未确认的 id。 pending_ids 记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符)。

以上内容引自 菜鸟 - Redis Stream

消息队列相关命令

  • XADD - 添加条目(entry)到末尾
  • XTRIM - 对流进行修剪。注意只能删除已经存在的条目,不能限制之后的条目的最大长度
  • XDEL - 删除条目
  • XLEN - 获取流包含的条目数量,即条目长度
  • XRANGE - 获取条目列表,会自动过滤已经删除的条目
  • XREVRANGE - 反向获取条目列表,ID 从大到小
  • XREAD - 以阻塞或非阻塞方式获取条目列表

消费者组相关命令

如何使用Stream消息队列

生产者写入消息 - XADD

XADD - 创建流(stream)并添加条目(entry)

XADD key [NOMKSTREAM] [MAXLEN|MINID [=|~] threshold [LIMIT count]] *|ID field value [field value ...]
  • key: 指定你的流键(key)名。将指定的流条目追加到指定键名的流中。

例如:

> XADD userstream * name devin age 1
  • 插入一个条目 “name devin age 1” 到流键名为 userstream 的流键中(流键名不存在则自动创建)

消费者读取消息 - XGROUP

XGROUP [CREATE key groupname ID|$ [MKSTREAM]] [SETID key groupname ID|$] [DESTROY key groupname] [CREATECONSUMER key groupname consumername]

该命令用于管理流数据结构关联的消费者组。使用 XGROUP 你可以:

  • 创建与流关联的新消费者组。
  • 销毁一个消费者组。
  • 从消费者组中移除指定的消费者。
  • 将消费者组的最后交付ID设置为其他内容。
创建消费者组 - XGROUP CREATE
XGROUP CREATE key groupname ID|$ [MKSTREAM]
  • key: 同 XADD 中的 key
  • groupname: 消费者组名
  • ID: 条目ID。特殊的,0-0(0) 表示从第一个开始的所有条目都是该消费者组的历史待消费消息,$ 表示在消费者组创建之后新加的条目,才属于该消费者组的历史待消费消息
  • MKSTREAM: 可选的。如果流键名不存在,则自动创建。
> XGROUP CREATE userstream consumer-group-1 0

另外,我们通过 XPENDING 查看消费者组中待处理条目的相关信息。

> XPENDING userstream consumer-group-1
1) (integer) 0
2) (nil)
3) (nil)
4) (nil)
  • 此时没有任何待处理条目
从消费者组中读取消息 - XREADGROUP
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
  • group: 同 XGROUP CREATE 中的 groupname
  • consumer: 消费者组中的一个消费者名,不存在则创建。

例如:

> XREADGROUP GROUP consumer-group-1 consumer-1 COUNT 1 BLOCK 2000 STREAMS userstream >
1) 1) "userstream"
   2) 1) 1) "1660094582148-0"
         2) 1) "name"
            2) "peter"
            3) "age"
            4) "3"
  • consumer-1: 消费者 consumer-1
  • COUNT 1: 只读取一个条目
  • STREAMS userstream >: 读取流 userstream
    1. 注意特殊ID >,这将会返回到目前为止流(userstream)从未传递给其他消费者(consumer-1以外的消费者)的新消息。不要跟第一个 > 搞混了,第一个 > 对于XREADGROUP命令来说没有任何意义,只是表明命令的开始。
    1. 任意其他的ID,即0或任意其他有效ID或不完整的ID(只有毫秒时间部分),将具有返回发送命令的消费者的待处理条目的效果。所以,基本上如果ID不是 > ,命令将让客户端访问它的待处理条目(已发送给它,但尚未确认的条目)。这种情况下,BLOCK 和 NOACK 将被忽略。
  • BLOCK 2000: 如果一直没有新的消息可以读取,阻塞等待两秒,超时则返回 null
    如果 COUNT 100,即读取100条目,阻塞并不会等待100条目,一旦有一个条目读取到,就立即停止阻塞返回结果。

我们再通过 XPENDING 查看消费者组中待处理条目的相关信息。

> XPENDING userstream consumer-group-1
1) (integer) 1
2) "1660094582148-0"
3) "1660094582148-0"
4) 1) 1) "consumer-1"
      2) "1"
  • 说明有一个条目待处理。
将待处理条目标记为"已处理" - XACK

如果我们确认一条消息已经处理,比如通过PHP或者其他语言处理了该消息,那么,我们可以用 XACK 将它标记,表示它不再是历史待处理消息的一部分,该消费者组内将不再报告它的任何消息。

  • XACK 标记后,不会删除条目,你仍然可以通过 XRANGE 读取它。
  • XACK 标记后,你仍然可以在别的消费者组内读取它。
XACK key group ID [ID ...]
  • key: 同 XADD 中的 key
  • group: 同 XGROUP CREATE 中的 groupname

例如:

> XACK userstream consumer-group-1 1660094582148-0

我们再通过 XPENDING 查看消费者组中待处理条目的相关信息。

> XPENDING userstream consumer-group-1
1) (integer) 0
2) (nil)
3) (nil)
4) (nil)
  • 说明上面的消息已经处理完毕(通过XACK标记的),此时没有任何待处理的消息
如何重新读取待处理条目

如果一个条目被消费者读取后,没有处理也没有被 XACK 标记为"已处理",那么他将保持待处理状态。
重新读取它可以使用实际ID 或者最小ID ‘0-0’,不要使用 >
例如:

> XREADGROUP GROUP consumer-group-1 consumer-1 COUNT 1 BLOCK 2000 STREAMS userstream 0-0
  • BLOCK 2000: 注意ID不是 > , BLOCK 不起效果,如果读不到,则立即返回空数组(empty array)

删除流条目

为什么处理完消息不立即删除,而是将其标记为已处理?

  1. Stream 支持重复消费,当存在多个消费者组时,不能立即删除
  2. Stream 删除后并没有立刻释放内存,反而可能增加内存占用。
    理解删除条目的底层细节
  • Redis流以一种使其内存高效的方式表示:使用基数树来索引包含线性数十个Stream条目的宏节点。通常,当你从Stream中删除一个条目的时候,条目并没有真正被驱逐,只是被标记为删除。
  • 最终,如果宏节点中的所有条目都被标记为删除,则会销毁整个节点,并回收内存。这意味着如果你从Stream里删除大量的条目,比如超过50%的条目,则每一个条目的内存占用可能会增加, 因为Stream将会开始变得碎片化。然而,流的表现将保持不变。
  • 在Redis未来的版本中,当一个宏节点内删除条目达到一定数量的时候,我们有可能会触发节点垃圾回收机制。目前,根据我们对这种数据结构的预期用途,还不太适合增加这样的复杂度。
  • 如果消费之后直接 XDEL 删除,那么这有可能增加内存占用。
  • 假如有一个已被 XREADGROUP 读取的条目但未被 XACK 标记已读, 此时它被 XDEL 删除了(实际上可能没被真正删除),你可能仍然可以通过 XREADGROUP 或者 XPENDING 读取它。
  1. 删除流的方式
    1. XTRIM 方式:只删除流数据,不删除 key。
      例如 XTRIM userstream 0
    1. DEL 方式:完全删除流,包括 key。
      例如 DEL userstream
      注意 DEL 方式会连带将消费者组删除。如果后台有消费者脚本运行,可能会报错。而且要重新创建消费者组。
删除只有一个消费者组的流条目

基于以上,我们删除流,采用一次性删除整个流的方案,具体如下:

建议使用 XTRIM 方式删除流数据。

  1. 查看流详细信息 XINFO STREAM userstream。重点看 last-generated-idgroups 字段
  2. 查看流是否存在关于它的消费者组 XINFO GROUPS userstream,判断是否存在待消费的消息(包含已读未消费的以及未读的)。重点看 pendinglast-delivered-id
  • 若不存在待消费的消息,则立刻删除流退出流程
  • 若存在待消费的消息,不删除流,但报告相关信息。
  1. 判断 XINFO STREAMXINFO GROUPS 结果中两者的 last-delivered-id 是否一致,不一致则说明存在未读的消息,我们记录并报告差异。
  2. 判断 XINFO GROUPS 结果中是否 pending 大于0,大于0则说明有已读未消费的消息。看步骤5
  3. 查看消费者组的消费者信息 XINFO CONSUMERS userstream consumer-group-1,报告每一个消费者的 pending 数量。
  4. 若 3,4,5中判断发现不存在待消费的消息,则立刻删除流退出流程
删除有多个消费者组的流条目

与 一个消费者组 的差别,就是我们要遍历所有消费者组,查看每一个消费者组是否存在待消费的消息

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值