Redis 5.0 Streams类型简介

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代表如果没有消息则一直等待,这个也可以设置等待超时的毫秒数。在最后面的$符号,代表只读取命令执行之后新来的消息。这里面COUNTBLOCK参数都是可选的,现在我们在终端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没有提供更多的可选项,它的功能和使用ListBL(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消息数,以及每一个consumerPending消息数和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

你可能感兴趣的:(Redis 5.0 Streams类型简介)