目录
概述
Stream 为 Redis 5.0 的一大亮点,可以功能完善的支持消息队列,是异步处理、应用解耦、限流削峰的一大利器。由于 Stream 涉及的知识点较多且出来的时间也不算长( 2018/10/17 正式 release),直接上来讲源码大家看着可能有些懵,于是把这一主题分为上、下两篇:上篇主要介绍 Stream 的应用,让大家对 Redis 的 Stream 作为消息队列有所了解;下篇就深入到源码分析,达到知其然并知其所以然。
添加(XADD)
起始版本:5.0.0
时间复杂度:O(log(N)) with N being the number of items already into the stream.
格式:XADD key ID field string [field string …]
向 Stream 中添加数据,如果 key 不存在,则创建。一般 XADD 这一端也叫做生产者,也就是不断向队列里生产消息。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> xadd blog * url blog.csdn.net id 0
QUEUED
127.0.0.1:6379> xadd blog * url blog.csdn.net id 1
QUEUED
127.0.0.1:6379> xadd blog * url blog.csdn.net id 2
QUEUED
127.0.0.1:6379> xadd blog * url blog.csdn.net id 3
QUEUED
127.0.0.1:6379> xadd blog * url blog.csdn.net id 4
QUEUED
127.0.0.1:6379> xadd blog * url blog.csdn.net id 5
QUEUED
127.0.0.1:6379> xadd blog * url blog.csdn.net id 6
QUEUED
127.0.0.1:6379> exec
1) "1587906311276-0"
2) "1587906311276-1"
3) "1587906311276-2"
4) "1587906311276-3"
5) "1587906311276-4"
6) "1587906311276-5"
7) "1587906311276-6"
上述命令创建了名叫 blog 的消息队列,队列里有 7 条消息。*
代表 Redis 内部创建消息 ID,格式为 毫秒数-序号
,如上因为一开始执行了 multi 命令,说明下面向 blog 消息队列添加消息都排着队,最后 exec 则表明把队列里的 xadd 依次执行,由于执行的速度快,都在 1587479823658 这个时间内执行完毕,于是为了区分,便有了 0 到 6 依次增长。Redis 内部生成的消息 ID 总结如下:
- ID 是由
-
分割为两部分,左边是 Redis Server 当前的毫秒时间戳,右边是一个无符号 64 位长整型序列号; - 同一个 KEY 下,后加入的 ID 一定要比已加入的 ID 大。
当然了,也可以自己定义消息 ID,但还是推荐使用 Redis 内置的。
读取(XREAD)
起始版本:5.0.0
时间复杂度:For each stream mentioned: O(log(N)+M) with N being the number of elements in the stream and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(log(N)). On the other side, XADD will pay the O(N) time in order to serve the N clients blocked on the stream getting new data.
格式:XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key …] ID [ID …]
从 Stream 中读取数据,依据是否设置 BLOCK 参数,大致可分为两类。
非阻塞读
127.0.0.1:6379> xread count 2 streams blog 0
1) 1) "blog"
2) 1) 1) "1587906311276-0"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "0"
2) 1) "1587906311276-1"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "1"
上述命令表示从名为 blog 的 streams 中读取两条记录,0 表示起始消息 ID 开始读
,也可以指定具体的消息 ID,表示自此消息 ID 之后开始读
,如下
127.0.0.1:6379> xread count 2 streams blog 1587906311276-1
1) 1) "blog"
2) 1) 1) "1587906311276-2"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "2"
2) 1) "1587906311276-3"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "3"
读取了两条消息,起始消息为 1587906311276-2,表明就是指定 1587906311276-1 之后开始读。
阻塞读
xread block 0 streams blog $
block 表示 XREAD 为阻塞读模式,0 代表不限制时间,$ 表示从最新的消息 ID 开始监听。
这时,再开一个终端,向 blog 里生产一条消息。
127.0.0.1:6379> xadd blog * url blog.csdn.net id 7
这时,在看监听的那个终端,会实时显示刚刚添加的那条消息,同时也会给出等待了多长时间。
127.0.0.1:6379> xread block 0 streams blog $
1) 1) "blog"
2) 1) 1) "1587906399333-0"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "7"
(15.53s)
也可以指定具体的毫秒数,表示在此时间内可以监听,一旦超时,则返回 nil。
127.0.0.1:6379> xread block 1000 streams blog $
(nil)
(1.07s)
最后简要说下各个参数:
- [COUNT count],用于限定获取的消息数量;
- [BLOCK milliseconds],用于设置 XREAD 为阻塞模式,默认为非阻塞模式;
- ID,用于设置由哪个消息 ID 开始读取。使用 0 表示从第一条消息开始,非 0 则表示从此消息之后开始读 count 个消息。在阻塞模式中,可以使用
$
,表示最新的消息 ID。(在非阻塞模式下 $ 无意义)。
范围查询(XRANGE)
起始版本:5.0.0
时间复杂度:O(log(N)+M) with N being the number of elements in the stream and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(log(N)).
格式: XRANGE key start end [ COUNT count ]
按照范围查询,相对应的还有 XREVRANGE,只不过是倒叙排。
127.0.0.1:6379> xrange blog - +
1) 1) "1587906311276-0"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "0"
2) 1) "1587906311276-1"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "1"
3) 1) "1587906311276-2"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "2"
4) 1) "1587906311276-3"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "3"
5) 1) "1587906311276-4"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "4"
6) 1) "1587906311276-5"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "5"
7) 1) "1587906311276-6"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "6"
8) 1) "1587906399333-0"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "7"
上述命令是从名叫 blog 的 Stream 中获取从 - 到 + 的 [COUNT count] 数据,其中 -、+ 表示最小和最大的消息 ID,也可以指定具体的消息 ID。
127.0.0.1:6379> xrange blog 1587906311276-0 1587906399333-0 count 2
1) 1) "1587906311276-0"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "0"
2) 1) "1587906311276-1"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "1"
如果 blog 这个消息队列数据多,想要迭代的话,则只需拿出每次消息 ID 加 +1 作为开始的消息 ID,结束消息 ID 为 +,再指定个数即可。
127.0.0.1:6379> xrange blog 1587906311276-0 + count 2
1) 1) "1587906311276-0"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "0"
2) 1) "1587906311276-1"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "1"
127.0.0.1:6379> xrange blog 1587906311276-2 + count 2
1) 1) "1587906311276-2"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "2"
2) 1) "1587906311276-3"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "3"
127.0.0.1:6379> xrange blog 1587906311276-4 + count 2
1) 1) "1587906311276-4"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "4"
2) 1) "1587906311276-5"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "5"
127.0.0.1:6379> xrange blog 1587906311276-6 + count 2
1) 1) "1587906311276-6"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "6"
2) 1) "1587906399333-0"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "7"
127.0.0.1:6379> xrange blog 1587906399333-1 + count 2
(empty list or set)
获取长度(XLEN)
起始版本:5.0.0
时间复杂度:O(1)
格式:XLEN KEY
获取流的消息数。
127.0.0.1:6379> xlen blog
(integer) 8
这里也可以看出,在上面操作了 xread 后,并没有把数据给删掉,还是 8 条。
删除消息(XDEL)
起始版本:5.0.0
时间复杂度:O(log(N)) with N being the number of items in the stream.
格式:XDEL key ID [ID …]
删除流里指定 ID 的消息。
127.0.0.1:6379> xdel blog 1587481956958-0
(integer) 1
127.0.0.1:6379> xlen blog
(integer) 7
删除之后,消息变成 7 条了。注意:这里并不是真正的删除,只是做了个删除标识,不展示而已。
消费组
前面提到的 XADD 可以当做生成者,向名为 blog 的消息队列不断生产消息,当多个消费者同时消费此消息队列时,会造成重复消费的问题。那么如何让多个消费者协同来消费呢,这就引出了 XGROUP 和 XREADGROUP,下面分别介绍这两个命令。
创建消费组(XGROUP )
起始版本:5.0.0
时间复杂度:O(log N) for all the subcommands, with N being the number of consumer groups registered in the stream, with the exception of the DESTROY subcommand which takes an additional O(M) time in order to delete the M entries inside the consumer group pending entries list (PEL).
格式:XGROUP [CREATE key groupname id-or- ] [ S E T I D k e y i d − o r − ] [SETID key id-or- ][SETIDkeyid−or−] [DESTROY key groupname] [DELCONSUMER key groupname consumername]
用于管理消费者组,提供创建组,销毁组,更新组起始消息ID等操作。
127.0.0.1:6379> xgroup create blog bgroup1 0
OK
在 blog 消息队列里创建了 1 个消费组,最后一个参数 0 表示该组从第一条消息开始消费;如果为为 $ 则表示该组将要消费当前时间开始的消息,也就是忽略该队列已存在的消息。
分组消费消息(XREADGROUP)
起始版本:5.0.0
时间复杂度:For each stream mentioned: O(log(N)+M) with N being the number of elements in the stream and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(log(N)). On the other side, XADD will pay the O(N) time in order to serve the N clients blocked on the stream getting new data.
格式:XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] STREAMS key [key …] ID [ID …]
分组消费消息操作。
127.0.0.1:6379> xreadgroup group bgroup1 c1 count 1 streams blog >
1) 1) "blog"
2) 1) 1) "1587906311276-0"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "0"
命令行最右边的 >
表示获取从来没有被分发的消息;c1
表示为 bgroup1
消费组的一个消费者。
127.0.0.1:6379> xreadgroup group bgroup1 c2 count 1 streams blog >
1) 1) "blog"
2) 1) 1) "1587906311276-1"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "1"
127.0.0.1:6379> xreadgroup group bgroup1 c2 count 1 streams blog >
1) 1) "blog"
2) 1) 1) "1587906311276-2"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "2"
127.0.0.1:6379> xreadgroup group bgroup1 c2 count 1 streams blog >
1) 1) "blog"
2) 1) 1) "1587906311276-3"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "3"
127.0.0.1:6379> xreadgroup group bgroup1 c3 count 1 streams blog >
1) 1) "blog"
2) 1) 1) "1587906311276-4"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "4"
127.0.0.1:6379> xreadgroup group bgroup1 c3 count 1 streams blog >
1) 1) "blog"
2) 1) 1) "1587906311276-5"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "5"
127.0.0.1:6379> xreadgroup group bgroup1 c3 count 1 streams blog >
1) 1) "blog"
2) 1) 1) "1587906311276-6"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "6"
分别创建了 c2
、c3
两个消费者,并分别消费了 3 条和 3 条消息。
至此,消费组 bgroup1 里有 c1、c2、c3 三个消费者,消费的消息分别为 c1->0、c2->1、c2->2、c2->3、c3->4、c3->5、c3->6。
等待列表(XPENDING)
起始版本:5.0.0
时间复杂度:O(log(N)+M) with N being the number of elements in the consumer group pending entries list, and M the number of elements being returned. When the command returns just the summary it runs in O(1) time assuming the list of consumers is small, otherwise there is additional O(N) time needed to iterate every consumer.
格式:XPENDING key group [start end count] [consumer]
上面在消费组 bgroup1 里创建了 c1、c2、c3 三个消费者,并把 blog 的队列里的消息都消费了,但是三个消费者并没有返回 XACK 确认消息,这时被消费且没有确认的消息都在待确认队列里(pending entry list,pel)。下面依据 XPENDING 格式来讲解。
- XPENDING key group
127.0.0.1:6379> XPENDING blog bgroup1 # 消费组 bgroup1 的待确认信息
1) (integer) 7 # 共 7 条
2) "1587906311276-0" # 起始消息 ID
3) "1587906311276-6" # 结束消息 ID
4) 1) 1) "c1" # 消费者 c1 有 1 个待确认消息
2) "1"
2) 1) "c2" # 消费者 c2 有 3 个待确认消息
2) "3"
3) 1) "c3" # 消费者 c3 有 3 个待确认消息
2) "3"
- XPENDING key group [start end count]
127.0.0.1:6379> XPENDING blog bgroup1 - + 10 # # 使用 start end count 选项可以获取各个消费组的详细信息
1) 1) "1587906311276-0" # 消息 ID
2) "c1" # 消费者
3) (integer) 1510957 # 从最后一次读取到现在闲置的毫秒数
4) (integer) 1 # 消息被读取了 1 次
2) 1) "1587906311276-1"
2) "c2"
3) (integer) 1085509
4) (integer) 1
3) 1) "1587906311276-2"
2) "c2"
3) (integer) 1084449
4) (integer) 1
4) 1) "1587906311276-3"
2) "c2"
3) (integer) 1083179
4) (integer) 1
5) 1) "1587906311276-4"
2) "c3"
3) (integer) 1078863
4) (integer) 1
6) 1) "1587906311276-5"
2) "c3"
3) (integer) 1077988
4) (integer) 1
7) 1) "1587906311276-6"
2) "c3"
3) (integer) 1077242
4) (integer) 1
- XPENDING key group [start end count] [consumer]
127.0.0.1:6379> XPENDING blog bgroup1 - + 10 c1 # 消费者 c1 Pending 列表
1) 1) "1587906311276-0" # 消息 ID
2) "c1" # 消费者
3) (integer) 2001045 # 从最后一次读取到现在闲置的毫秒数
4) (integer) 1 # 消息被读取了 1 次
127.0.0.1:6379> XPENDING blog bgroup1 - + 10 c2 # 消费者 c2 Pending 列表
1) 1) "1587906311276-1" # 消息 ID
2) "c2" # 消费者
3) (integer) 1577628 # 从最后一次读取到现在闲置的毫秒数
4) (integer) 1 # 消息被读取了 1 次
2) 1) "1587906311276-2"
2) "c2"
3) (integer) 1576568
4) (integer) 1
3) 1) "1587906311276-3"
2) "c2"
3) (integer) 1575298
4) (integer) 1
127.0.0.1:6379> XPENDING blog bgroup1 - + 10 c3 # 消费者 c3 Pending 列表
1) 1) "1587906311276-4" # 消息 ID
2) "c3" # 消费者
3) (integer) 1572390 # 从最后一次读取到现在闲置的毫秒数
4) (integer) 1 # 消息被读取了 1 次
2) 1) "1587906311276-5"
2) "c3"
3) (integer) 1571515
4) (integer) 1
3) 1) "1587906311276-6"
2) "c3"
3) (integer) 1570769
4) (integer) 1
这里会发现,pel 列表中的每一个每个 Pending 的消息有 4 个属性
- 消息ID
- 所属消费者
- 空闲时长
- 消息被读取次数【列表里的消息时可以重复消费的】
确认消息(XACK)
起始版本:5.0.0
时间复杂度:O(log N) for each message ID processed.
格式:XACK key group ID [ID …]
使用命令 XACK 完成告知消息处理完成。
127.0.0.1:6379> xack blog bgroup1 1587906311276-0
(integer) 1
这时在查看 c1 消费者的 pel。
127.0.0.1:6379> XPENDING blog bgroup1 - + 10 c1
(empty list or set)
为空,说明都已经确认完毕。
127.0.0.1:6379> xack blog bgroup1 1587906311276-1
(integer) 1
127.0.0.1:6379> XPENDING blog bgroup1 - + 10 c2
1) 1) "1587906311276-2"
2) "c2"
3) (integer) 2360843
4) (integer) 1
2) 1) "1587906311276-3"
2) "c2"
3) (integer) 2359573
4) (integer) 1
消费者 c2 里确认了一条,然后发现待确认队列里只剩两条了。
消息转移(XCLAIM)
起始版本:5.0.0
时间复杂度:O(log N) with N being the number of messages in the PEL of the consumer group.
格式:XCLAIM key group consumer min-idle-time ID [ID …] [IDLE ms] [TIME ms-unix-time] [RETRYCOUNT count] [FORCE] [JUSTID]
如果消费者 c2 挂掉了,这时可以通过 XCLAIM 把 c2 中未被确认的消息重新声明给其他消费者,比如 c3。
127.0.0.1:6379> xclaim blog bgroup1 c3 30000 1587906311276-2
1) 1) "1587906311276-2"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "2"
127.0.0.1:6379> xclaim blog bgroup1 c3 30000 1587906311276-3
1) 1) "1587906311276-3"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "3"
上述命令可以将将原本属于消费者c2 的消息 1587906311276-2、1587906311276-3 在等待确认的时间大于 30000 毫秒情况下重新声明给 c3 消费者。
127.0.0.1:6379> xpending blog bgroup1 - + 10 c2
(empty list or set)
这时发现消费者 c2 的 pel 为空了。
127.0.0.1:6379> xpending blog bgroup1 - + 10 c3
1) 1) "1587906311276-2"
2) "c3"
3) (integer) 205849
4) (integer) 2
2) 1) "1587906311276-3"
2) "c3"
3) (integer) 198978
4) (integer) 2
3) 1) "1587906311276-4"
2) "c3"
3) (integer) 2961488
4) (integer) 1
4) 1) "1587906311276-5"
2) "c3"
3) (integer) 2960613
4) (integer) 1
5) 1) "1587906311276-6"
2) "c3"
3) (integer) 2959867
4) (integer) 1
消费者 c3 的 pel 多了 2 条,共 5 条了。
消息上限(XTRIM)
起始版本:5.0.0
时间复杂度:O(log(N)) + M, with N being the number of entries in the stream prior to trim, and M being the number of evicted entries. M constant times are very small however, since entries are organized in macro nodes containing multiple entries that can be released with a single deallocation.
格式:XTRIM key MAXLEN [~] count
Stream 消息太多怎么办,有没有办法去掉些,这时可以使用 XTRIM 来裁剪。
127.0.0.1:6379> xtrim blog maxlen 5
(integer) 2
删除了 2 两条,再来看看 blog 里的数据。
127.0.0.1:6379> xrange blog - + count 10
1) 1) "1587906311276-2"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "2"
2) 1) "1587906311276-3"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "3"
3) 1) "1587906311276-4"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "4"
4) 1) "1587906311276-5"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "5"
5) 1) "1587906311276-6"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "6"
发现 XTRIM 删掉的是消息 ID 从小到的 count 条数。其实,这个命令和 XADD 时加上 MAXLEN 是一样的。
127.0.0.1:6379> xadd blog maxlen 5 * url blog.csdn.net id 8
"1587912344636-0"
127.0.0.1:6379> xlen blog
(integer) 5
127.0.0.1:6379> xrange blog - + count 10
1) 1) "1587906311276-3"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "3"
2) 1) "1587906311276-4"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "4"
3) 1) "1587906311276-5"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "5"
4) 1) "1587906311276-6"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "6"
5) 1) "1587912344636-0"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "8"
信息监控(XINFO)
起始版本:5.0.0
时间复杂度:O(N) with N being the number of returned items for the subcommands CONSUMERS and GROUPS. The STREAM subcommand is O(log N) with N being the number of items in the stream.
格式:XINFO [CONSUMERS key groupname] key key [HELP]
Stream 提供了 XINFO 来实现对服务器信息的监控。
- 查看队列信息
127.0.0.1:6379> xinfo stream blog
1) "length" # 消息长度 7
2) (integer) 7
3) "radix-tree-keys" # 1 棵 radix 基数树
4) (integer) 1
5) "radix-tree-nodes" # 2 个 基数树节点
6) (integer) 2
7) "groups" # 1 个组
8) (integer) 1
9) "last-generated-id" # 最后一条生成的消息 ID,注这个在上面演示 xdel 时删除了实体消息,但消息 ID 还在
10) "1587906399333-0"
11) "first-entry" # 第一条消息
12) 1) "1587906311276-0"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "0"
13) "last-entry" # 最后一条消息
14) 1) "1587906311276-6"
2) 1) "url"
2) "blog.csdn.net"
3) "id"
4) "6"
- 消费组信息
127.0.0.1:6379> xinfo groups blog
1) 1) "name" # 名称为 bgroup1
2) "bgroup1"
3) "consumers" # 有 3 个消费组
4) (integer) 3
5) "pending" # 待确认队列里有 5 条消息
6) (integer) 5
7) "last-delivered-id" # 最后一条消息 ID
8) "1587906311276-6"
- 消费者组成员信息
127.0.0.1:6379> xinfo consumers blog bgroup1
1) 1) "name"
2) "c1"
3) "pending" # 待确认队列里有 0 条消息
4) (integer) 0
5) "idle" # 从最后一次读取到现在闲置的毫秒数
6) (integer) 1485181
2) 1) "name"
2) "c2"
3) "pending" # 待确认队列里有 0 条消息
4) (integer) 0
5) "idle" # 从最后一次读取到现在闲置的毫秒数
6) (integer) 1012027
3) 1) "name"
2) "c3"
3) "pending" # 待确认队列里有 5 条消息
4) (integer) 5
5) "idle" # 从最后一次读取到现在闲置的毫秒数
6) (integer) 820213