文章目录
基于 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 - 以阻塞或非阻塞方式获取条目列表
消费者组相关命令
- XGROUP - 管理流数据结构关联的消费者组
- XGROUP CREATE - 创建消费者组
- XGROUP SETID - 为消费者组设置新的最后递送条目ID
- XGROUP DELCONSUMER - 删除消费者
- XGROUP DESTROY - 删除消费者组
- XREADGROUP GROUP - 读取消费者组中的条目并将其标记为"待处理"状态
- XACK - 将待处理条目标记为"已处理"
- XPENDING - 显示待处理条目的相关信息
- XCLAIM - 转移待处理条目的归属权
- XAUTOCLAIM - 转移与指定条件匹配的待处理条目的所有权(Redis v6.2)
- XINFO - 查看流和消费者组的相关信息
- XINFO STREAM - 查看流信息
- XINFO GROUPS - 查看消费者组的信息
- XINFO CONSUMERS - 查看消费者组中的消费者信息
如何使用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
-
- 注意特殊ID >,这将会返回到目前为止流(userstream)从未传递给其他消费者(consumer-1以外的消费者)的新消息。不要跟第一个 > 搞混了,第一个 > 对于XREADGROUP命令来说没有任何意义,只是表明命令的开始。
-
- 任意其他的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)
删除流条目
为什么处理完消息不立即删除,而是将其标记为已处理?
- Stream 支持重复消费,当存在多个消费者组时,不能立即删除
- Stream 删除后并没有立刻释放内存,反而可能增加内存占用。
理解删除条目的底层细节
- Redis流以一种使其内存高效的方式表示:使用基数树来索引包含线性数十个Stream条目的宏节点。通常,当你从Stream中删除一个条目的时候,条目并没有真正被驱逐,只是被标记为删除。
- 最终,如果宏节点中的所有条目都被标记为删除,则会销毁整个节点,并回收内存。这意味着如果你从Stream里删除大量的条目,比如超过50%的条目,则每一个条目的内存占用可能会增加, 因为Stream将会开始变得碎片化。然而,流的表现将保持不变。
- 在Redis未来的版本中,当一个宏节点内删除条目达到一定数量的时候,我们有可能会触发节点垃圾回收机制。目前,根据我们对这种数据结构的预期用途,还不太适合增加这样的复杂度。
- 如果消费之后直接 XDEL 删除,那么这有可能增加内存占用。
- 假如有一个已被 XREADGROUP 读取的条目但未被 XACK 标记已读, 此时它被 XDEL 删除了(实际上可能没被真正删除),你可能仍然可以通过 XREADGROUP 或者 XPENDING 读取它。
- 删除流的方式
-
- XTRIM 方式:只删除流数据,不删除 key。
例如XTRIM userstream 0
- XTRIM 方式:只删除流数据,不删除 key。
-
- DEL 方式:完全删除流,包括 key。
例如DEL userstream
注意 DEL 方式会连带将消费者组删除。如果后台有消费者脚本运行,可能会报错。而且要重新创建消费者组。
- DEL 方式:完全删除流,包括 key。
删除只有一个消费者组的流条目
基于以上,我们删除流,采用一次性删除整个流的方案,具体如下:
建议使用 XTRIM 方式删除流数据。
- 查看流详细信息
XINFO STREAM userstream
。重点看 last-generated-id 和 groups 字段 - 查看流是否存在关于它的消费者组
XINFO GROUPS userstream
,判断是否存在待消费的消息(包含已读未消费的以及未读的)。重点看 pending 和 last-delivered-id
- 若不存在待消费的消息,则立刻
删除流后退出流程。 - 若存在待消费的消息,不删除流,但报告相关信息。
- 判断
XINFO STREAM
和XINFO GROUPS
结果中两者的 last-delivered-id 是否一致,不一致则说明存在未读的消息,我们记录并报告差异。 - 判断
XINFO GROUPS
结果中是否 pending 大于0,大于0则说明有已读未消费的消息。看步骤5 - 查看消费者组的消费者信息
XINFO CONSUMERS userstream consumer-group-1
,报告每一个消费者的 pending 数量。 - 若 3,4,5中判断发现不存在待消费的消息,则立刻
删除流后退出流程。
删除有多个消费者组的流条目
与 一个消费者组 的差别,就是我们要遍历所有消费者组,查看每一个消费者组是否存在待消费的消息