2 Redis Stream
Redis5.0 最大的新特性就是多出了一个数据结构 Stream,它是一个新的强大的支持多播的可持久化的消息队列
stream总述
Redis Stream 的结构如上图所示,每一个Stream都有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容。消息是持久化的,Redis 重启后,内容还在。
具体玩法如下
1:每个stream都有唯一的名称,就是key.
消息 ID 的形式是timestampInMillis-sequence,例如1527846880572-5,它表示当前的消息在毫米时间戳1527846880572时产生,并且是该毫秒内产生的第 5 条消息。消息 ID 可以由服务器自动生成(代表默认动),也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面加入的消息的 ID 要大于前面的消息 ID。
消息内容就是键值对,形如 hash 结构的键值对,
2:每个Stream都可以挂在多个消费者,每个消费者会有个游标last_delivered_id在Stream数组之上往前移动,表示当前消费者已经消费到哪条消息了。
每个消费组都有一个Stream 内唯一的名称,消费组不会自动创建,它需要单独的指令xgroup create进行创
建,需要指定从 Stream 的某个消息 ID 开始消费,这个 ID 用来初始化last_delivered_id变量
3:每个消费组(Consumer Group)的状态都是独立的,相互不受影响,
4:同一个消费组 (Consumer Group) 可以挂接多个消费者 (Consumer),这些消费者之间是竞争关系,任意
一个消费者读取了消息都会使游标last_delivered_id往前移动。每个消费者有一个组内唯一名称。
5:消费者 (Consumer) 内部会有个状态变量pending_ids,它记录了当前已经被客户端读取,但是还没有 ack
的消息。如果客户端没有 ack,这个变量里面的消息 ID 会越来越多,一旦某个消息被 ack,它就开始减少。
这个 pending_ids 变量在 Redis 官方被称之为PEL,也就是Pending Entries List,这是一个很核心的数据结
构,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。
常用操作的命令
生产端
- xadd命令:
*号表示服务器自动生成 ID,后面顺序跟着一堆 key/value。
1713788135332-0 则是生成的消息 ID,由两部分组成:时间戳-序号。时间戳时毫秒级单位,是生成消息的
Redis服务器时间,它是个64位整型。序号是在这个毫秒时间点内的消息序号。它也是个64位整型。为了保证消息是有序的,因此Redis生成的ID是单调递增有序的。由于ID中包含时间戳部分,为了避免服务器
时间错误而带来的问题(例如服务器时间延后了),Redis的每个Stream类型数据都维护一个
latest_generated_id属性,用于记录最后一个消息的ID。若发现当前时间戳退后(小于latest_generated_id
所记录的),则采用时间戳不变而序号递增的方案来作为新消息ID(这也是序号为什么使用int64的原因,保
证有足够多的的序号),从而保证ID的单调递增性质。
强烈建议使用Redis的方案生成消息ID,因为这种时间戳+序号的单调递增的ID方案,几乎可以满足你全部的
需求。但ID是支持自定义的。
2.xrange命令:获取消息列表,会自动过滤已经删除的消息
xrange stream - + 其中-表示最小值 , + 表示最大值
del删除Stream (删除整个Stream)
xdel可以删除指定的消息(指定ID)
消费端
单消费端
虽然Stream中有消费者组的概念,但是可以在不定义消费组的情况下进行 Stream 消息的独立消费,当
Stream 没有新消息时,甚至可以阻塞等待。Redis 设计了一个单独的消费指令xread,可以将 Stream 当成
普通的消息队列 (list) 来使用。使用 xread 时,我们可以完全忽略消费组 (Consumer Group) 的存在,就好
比 Stream 就是一个普通的列表 (list)。
xread count 1 streams stream1 0-0 表示从 Stream 头部读取1条消息,0-0指从头开始
xread count 1 streams stream1 $
$代表从尾部读取,上面的意思就是从尾部读取最新的一条消息,此时默认不返回任何消息
应该以阻塞的方式读取尾部最新的一条消息,直到新的消息的到来
比如如下:
block0表示一直阻塞
群组消费
创建消费者组
消费组消费
xreadgroup GROUP c1 consumer1 count 1 streams stream1 >
Stream需要考虑的问题
1:Stream消息太多怎么办?
要是消息积累太多,Stream 的链表岂不是很长,内容会不会爆掉?xdel 指令又不会删除消息,它只是给消息
做了个标志位。Redis 自然考虑到了这一点,所以它提供了一个定长 Stream 功能。在 xadd 的指令提供一个定长长度maxlen,就可以将老的消息干掉,确保最多不超过指定长度。
2:消息如果忘记ACK会怎么样?
Stream 在每个消费者结构中保存了正在处理中的消息 ID 列表 PEL,如果消费者收到了消息处理完了但是没
有回复 ack,就会导致 PEL 列表不断增长,如果有很多消费组的话,那么这个 PEL 占用的内存就会放大。所
以消息要尽可能的快速消费并确认。
3:PEL如何避免消息丢失
在客户端消费者读取 Stream 消息时,Redis 服务器将消息回复给客户端的过程中,客户端突然断开了连
接,消息就丢失了。但是 PEL 里已经保存了发出去的消息 ID。待客户端重新连上之后,可以再次收到 PEL
中的消息 ID 列表。不过此时 xreadgroup 的起始消息 ID 不能为参数>,而必须是任意有效的消息 ID,一般
将参数设为 0-0,表示读取所有的 PEL 消息以及自last_delivered_id之后的新消息。
2.1 Redis中几种消息队列实现的总结
基于List的LPUSH+BRPOP 的实现
足够简单,消费消息的延迟几乎为0,但是需要处理空闲连接的问题
如果线程一直阻塞在那里,Redis客户端的连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减
少闲置资源占用,这个时候blpop和brpop或抛出异常,所以在编写客户端消费者的时候要小心,如果捕获
到异常,还有重试。
其他缺点包括:
消费组确认ACK比较复杂,不能保证消费组消费消息后是否成功处理的问题。通常需要维护一个Pending列表,保证消息处理确认;不能做广播模式;不能重复消费,一旦消费就会被删除;不支持分组消费
基于Sorted-Set的实现
多用来实现延迟队列,当然也可以实现有序的普通的消息队列,但是消费者无法阻塞的获取消息,只能轮
询,不允许重复消息。
PUB/SUB,订阅/发布模式
优点:典型的广播模式,一个消息可以发布到多个消费者;多信道订阅,消费者可以同时订阅多个信道,从而接收
多类消息;消息即时发送,消息不用等待消费者读取,消费者会自动接收到信道发布的消息。
缺点:消息一旦发布,不能接收。换句话就是发布时若客户端不在线,则消息丢失,不能寻回;不能保证每个消费
者接收的时间是一致的;若消费者客户端出现消息积压,到一定程度,会被强制断开,导致消息意外丢失。
通常发生在消息的生产远大于消费速度时;可见,Pub/Sub 模式不适合做消息存储,消息积压类的业务,而
是擅长处理广播,即时通讯,即时反馈的业务。
基于Stream类型的实现
大体上没问题