Redis在5.0版本最大的特性是支持一种新的数据类型Streams
。针对这个新的数据类型,Redis在底层也加了新的数据结构来支持。从功能层面来讲,Streams
加上它的指令实现了一个完备的分布式消息队列。它支持Consumer Group
,熟悉Kafka
或者RocketMQ
等分布式消息队列的应该知道这个概念,通过将多个客户端加入同一个Consumer Group
,可以实现多个客户端相互配合,每个消费其中一部分消息的功能。
之前我们使用Redis做消息队列,一般是用List
或者Pub/Sub
,这两个数据结构(命令)的缺点是比较明显的,最大的一点是不支持消费确认,无论数据在客户端处理失败还是成功,都不会再次拿到这条数据。List
不支持一条数据给多个客户端,要实现这个功能只能发送端重复发送到不同Key的List
,而Pub/Sub
的问题是如果client长时间不在线,大部分消息都会丢失。所以,这两种方式都不是为了消息队列而设计的,而这些问题Streams
数据类型都给出了完整的解决方案。
发送消息到Streams
XADD指令用来发送一条消息,后面跟一个数据结构。比如发送一个用户的数据:
127.0.0.1:6379> xadd userstream * name Peter age 25
"1552786438518-0"
127.0.0.1:6379>
userstream
是Redis key,也就是Streams
的名字,*
代表让redis自动生成消息id,后面就是属性名和值的数据,这条指令的返回值就是Redis自动生成的id,格式是
,前面一段是毫秒数,后面sequenceNumber
一般是0,如果同一毫秒有多条消息,则依次+1。
这样生成id的好处是Redis支持用id的范围来查询消息,使用时间则id必然是自增的。这里我们可能有个问题,如果我把服务器的时间往前调id是不是就乱了?答案是不会的,Redis会自动检查发现时间变到以前了,那就不会用系统时钟来生成id,而是用之前最大的那个时间来生成,直到系统时间追上原来的时间。
在发送消息时可以自己指定消息id,比如我们可以指定的相同毫秒数但sequenceNumber
不同的id:
127.0.0.1:6379> xadd userstream 1552786438518-1 name Monster age 18
"1552786438518-1"
127.0.0.1:6379>
自己指定的id也要遵循递增原则,比如加一条id 为0-1的就会报错。
127.0.0.1:6379> xadd userstream 0-1 name Jay age 28
(error) ERR The ID specified in XADD is equal or smaller than the target stream top item
127.0.0.1:6379>
我们换个streams就可以正常添加:
127.0.0.1:6379> xadd userstream2 0-1 name Jay age 28
"0-1"
127.0.0.1:6379>
这里还有一点要注意就是,0-0
这个id是系统默认的最小id,所以即使是一个全新的Streams
,也不能使用这个id 来添加消息。
XLEN指令可以查询当前Streams
中有多少条数据
127.0.0.1:6379> XLEN userstream
(integer) 3
127.0.0.1:6379>
查询Stream中的消息
数据添加到Streams
后可以使用XRANGE命令可以用来查询Streams
中的历史消息,后面跟起始id和结束id,命令如下:
127.0.0.1:6379> XRANGE userstream - +
1) 1) "1552745982069-0"
2) 1) "name"
2) "Tony"
3) "age"
4) "20"
2) 1) "1552786438518-0"
2) 1) "name"
2) "Peter"
3) "age"
4) "25"
3) 1) "1552786438518-1"
2) 1) "name"
2) "Monster"
3) "age"
4) "18"
127.0.0.1:6379>
这里的-
和+
代表最小id和最大id,如果不知道Streams
中的id范围的话,第一次可以用这两个。当然也可以直接指定起始和结束id,而且不需要是一个准确的值,这么做的好处是如果id是使用Redis自动生成的,则可以根据时间段来查询:
127.0.0.1:6379> XRANGE userstream 1552786438518 +
1) 1) "1552786438518-0"
2) 1) "name"
2) "Peter"
3) "age"
4) "25"
2) 1) "1552786438518-1"
2) 1) "name"
2) "Monster"
3) "age"
4) "18"
127.0.0.1:6379>
XRANGE
命令可以返回消息id和属性值,如果我们不知道id的范围而直接使用- +
的话可能数据量很大。这种情况,可以使用COUNT
达到翻页的效果。
127.0.0.1:6379> XRANGE userstream 1552786438518 + COUNT 1
1) 1) "1552786438518-0"
2) 1) "name"
2) "Peter"
3) "age"
4) "25"
127.0.0.1:6379>
大部分情况下,我们对于消息队列的使用场景都不是简单的范围查询,而是能够实时接收到队列中的新消息,这个可以使用XREAD
命令来实现
监听新消息
XREAD命令用来监听Streams
中的新的消息,它可以使用阻塞的模式等待新消息的到来,如果多个客户端同时Block
在一个Streams
上,Redis将使用先进先出(FIFO)的策略来决定先响应哪个客户端。
终端1:
127.0.0.1:6379> XREAD COUNT 1 BLOCK 0 STREAMS userstream $
在终端1上我们使用阻塞模式读取消息,每次读取一条,BLOCK
后面的0代表如果没有消息则一直等待,这个也可以设置等待超时的毫秒数。在最后面的$
符号,代表只读取命令执行之后新来的消息。这里面COUNT
和BLOCK
参数都是可选的,现在我们在终端2上往Streams
中加一条消息。
终端2:
127.0.0.1:6379> xadd userstream * name Ramon age 30
"1552788470797-0"
127.0.0.1:6379>
终端1就会收到这条数据,并且返回一共BLOCK了多长时间:
127.0.0.1:6379> XREAD COUNT 1 BLOCK 0 STREAMS userstream $
1) 1) "userstream"
2) 1) 1) "1552788470797-0"
2) 1) "name"
2) "Ramon"
3) "age"
4) "30"
(498.12s)
127.0.0.1:6379>
XREAD
支持同时监听多个streams
中的消息,无论哪个Stream
中有新的消息,都会返回。
除了可以控制返回消息的数量和使用阻塞模式,XREAD
没有提供更多的可选项,它的功能和使用List
的BL(R)POP
命令有点类似,只是它可以将一条消息投递给多个Client
。更复杂的消费功能是由XREADGOUP
命令来实现的
Consumer Group
对于分布式消息队列的使用者来说,XREAD
提供的功能不是最重要的,大部分场景下我们需要多个Client
作为一个集群来消费消息,所以Streams
也和Kafka
一样提供了Consumer Group
的功能,同一个Group
中的一个Consumer
只消费Streams
中的一部分消息。
创建Consumer Group
在使用Consumer Group
之前必须先使用XGROUP
命令创建,group是关联到Streams上的,创建时必须先保证Streams已经存在了。
127.0.0.1:6379> XGROUP CREATE userstream cgroup 0-0
OK
127.0.0.1:6379>
在创建group的时候需要指定从哪条消息开始消费,这里使用0-0
,则group创建前的消息也会收到,如果只想要group创建之后的消息,则可以使用$
符号
使用集群模式消费
Consumer
使用XREADGROUP
命令以集群方式消费消息,同样支持阻塞和非阻塞模式
127.0.0.1:6379> XREADGROUP GROUP cgroup c1 COUNT 1 BLOCK 5000 STREAMS userstream >
1) 1) "userstream"
2) 1) 1) "1552745982069-0"
2) 1) "name"
2) "Tony"
3) "age"
4) "20"
127.0.0.1:6379>
命令中group名字之后的c1
代表当前consumer的名字,这里出现了一个新的符号‘>’,代表从来没有投递给其它客户端的消息。大部分情况下我们都是希望Consumer
按这种方式来消费。因为队列中有消息,所以这个命令不会阻塞直接返回了,我们再执行一遍:
127.0.0.1:6379> XREADGROUP GROUP cgroup c1 COUNT 1 BLOCK 5000 STREAMS userstream >
1) 1) "userstream"
2) 1) 1) "1552786438518-0"
2) 1) "name"
2) "Peter"
3) "age"
4) "25"
127.0.0.1:6379>
可以看到这里收到的就会是Streams
中的第2条数据。现在看下如果最后的字符不是'>'而是一个起始的id,比如0-0:
127.0.0.1:6379> XREADGROUP GROUP cgroup c1 COUNT 1 BLOCK 5000 STREAMS userstream 0-0
1) 1) "userstream"
2) 1) 1) "1552745982069-0"
2) 1) "name"
2) "Tony"
3) "age"
4) "20"
127.0.0.1:6379>
发现又是第一条消息,这是因为在group模式下,如果最后id不是>
,则收到的消息将是之前已经投递给这个Consumer
,但是并未收到ACK
消息的,Redis称之为Pending消息。
提供这个功能的目的是为了解决Consumer
非正常Crash
的情况,比如一个Consumer
非正常重启,我们可以先循环发送带id的XREADGROUP
命令,直到没有新的Pending
消息,然后在发送'>'消息。现在我们把之前的第1,2条消息都确认掉,就会发现Streams
中是空的了,这代表没有Pending
消息,而不是没有未消费消息:
127.0.0.1:6379> XACK userstream cgroup 1552745982069-0 1552786438518-0
(integer) 2
127.0.0.1:6379> XREADGROUP GROUP cgroup c1 STREAMS userstream 0-0
1) 1) "userstream"
2) (empty list or set)
127.0.0.1:6379>
从上面的逻辑有没有看到一个问题,就是如果我们这个Consumer c1
永远挂了,然后又存在Pending
消息,这些消息该怎么办?Redis提供了另外一组命令来处理这种情况。
读取Pending消息并恢复
使用XPENDING
命令,客户端可以读取某个ConsumerGroup
下的所有Pending
消息,这个命令是只读的,不会改变消息的状态,所以可以重复执行。当然也可以指定只获取group下面某一个consumer的pending消息。
127.0.0.1:6379> XREADGROUP GROUP cgroup c1 STREAMS userstream >
1) 1) "userstream"
2) 1) 1) "1552786438518-1"
2) 1) "name"
2) "Monster"
3) "age"
4) "18"
2) 1) "1552788470797-0"
2) 1) "name"
2) "Ramon"
3) "age"
4) "30"
127.0.0.1:6379>
首先,读取所有消息但是不XACK
,然后查询Pending
消息:
127.0.0.1:6379> XPENDING userstream cgroup - + 10
1) 1) "1552786438518-1"
2) "c1"
3) (integer) 65025
4) (integer) 1
2) 1) "1552788470797-0"
2) "c1"
3) (integer) 65025
4) (integer) 1
127.0.0.1:6379>
可以看到有2条处于Pending
状态的消息,属于c1
这个consumer,里面的65025
代表了pending了多长时间,最后一个1
代表这条消息的被投递次数。
在拿到Pending消息后,我们就可以使用XCLAIM
命令来将消息的归属权给到别的consumer。
127.0.0.1:6379> XCLAIM userstream cgroup c2 1000 1552786438518-1
1) 1) "1552786438518-1"
2) 1) "name"
2) "Monster"
3) "age"
4) "18"
127.0.0.1:6379> XPENDING userstream cgroup - + 10
1) 1) "1552786438518-1"
2) "c2"
3) (integer) 5226
4) (integer) 1
2) 1) "1552788470797-0"
2) "c1"
3) (integer) 492957
4) (integer) 1
127.0.0.1:6379>
控制Streams的容量
Streams中的消息不会因为消费而被移除,大部分情况下我们不会想要消息永久的存储在Redis中,这是很浪费资源的。Stream的XADD
命令提供了一个参数MAXLEN
来控制Streams的最大长度,只要在发送新消息时带上这个参数,就可以控制Streams中保存的总的消息数量。
127.0.0.1:6379> XLEN userstream
(integer) 5
127.0.0.1:6379> XADD userstream MAXLEN 3 * name Jack age 23
"1552790834734-0"
127.0.0.1:6379> XLEN userstream
(integer) 3
127.0.0.1:6379>
如果只是想控制一下Streams中的消息量而不添加,可以使用XTRIM
命令,由名字就可以看出是用来裁剪长度的。有时候我们也许并不像把队列长度控制的那么精确,而是是一个大概的数字,比如1000
左右,那可以在MAXLEN
和数字中间加一个~
,这样可以提高命令执行的性能
127.0.0.1:6379> XTRIM userstream MAXLEN ~ 2
(integer) 1
127.0.0.1:6379>
监控
对于生产上的基础服务,监控是非常重要的,Streams提供了XINFO
命令来查看Streams
的状态。比如可以看当前Streams的length
,总共有多少个consumer group
。针对每一个consumer group
,可以查看consumer
的数量和pending
消息数,以及每一个consumer
的Pending
消息数和idle
时间。具体用法可以参考官方文档。
127.0.0.1:6379> XINFO CONSUMERS userstream cgroup
1) 1) "name"
2) "c1"
3) "pending"
4) (integer) 1
5) "idle"
6) (integer) 817873
2) 1) "name"
2) "c2"
3) "pending"
4) (integer) 1
5) "idle"
6) (integer) 787873
127.0.0.1:6379>
消息删除
通常对于一个消息队列来说,删除通常不是一个必需的功能。Streams
提供了XDEL
命令来删除指定id的消息。当然这个功能不会真的物理删除,所以不要在应用中依赖它来释放空间。
127.0.0.1:6379> XRANGE userstream - +
1) 1) "1552790802580-0"
2) 1) "name"
2) "Jessie"
3) "age"
4) "19"
2) 1) "1552790834734-0"
2) 1) "name"
2) "Jack"
3) "age"
4) "23"
127.0.0.1:6379> XDEL userstream 1552790802580-0
(integer) 1
127.0.0.1:6379> XRANGE userstream - +
1) 1) "1552790834734-0"
2) 1) "name"
2) "Jack"
3) "age"
4) "23"
127.0.0.1:6379>
总结
相对于过去依赖List
来简单的模拟消息队列的功能,Streams
显然是官方提供的更直接更强大的功能(虽然个人觉得Pending消息的XCLAIM
那里有点繁琐)。对于5.0之后使用Redis
做消息队列,毫无疑问Streams
是个更好的选择。
[参考资料]
Introduction to Redis Streams