概述
在Redis5.0(GA October 2018)之后提供的了流这个数据结构,它以更加抽象的方式对日志数据结构重新建模。尽管日志本质上依然完备:类似日志文件一样,经常实现类似以append-only模式打开的的文件。Redis Streams首先是一个只能追加的数据结构。由于是一种内存抽象数据类型,至少在理论上,因此实现了强大处理能力能克服日志文件的限制。
Streams
get start
是什么让Redis streams成为Redis里面最复杂的类型,抛开它自身非常简单的数据结构,事实上是它实现了附加的,非强制的特性:一个阻塞操作允许消费者等待新数据被生产者增加到stream里面,此附加的概念叫做Consumer Groups
。
消费组最初被引入是被流行的消息系统Kafka提出的。Redis通过完全不同的角度重新实现了类似的想法,但是目标是一致的:允许一组不同的clients去消费相同的stream消息。然而,注意,List也有一个可选的更复杂的阻塞API,通过类似BLPOP命令。所以Streams在规则上和Lists没有太多区别,它只是增加了附加API变得更加复杂和强大。
streams basics
为了了解Redis Streams是什么,如何使用它们,我们将忽略一些高级特性,将聚焦它本身的数据结构,从命令行的角度介绍访问维护它的惯用方法。基本上这些方法,在大多数Redis数据结构共有的部分,比如list,Sets,Sorted Set等。
因为Streams是只能附加的数据结构,使用XADD
写命令,附加项到指定的stream。项不仅仅只是string
,也能填充一个或者多个域-值模式数据。这样,里的每个条目都已经结构化,例如以CSV格式编写的附加文件,其中每行中存在多个单独的段。
> XADD mystream * sensor-id 1234 temperatur 19.8
> (output): 1518951480106-0
上面的命令将会附加sensor-id: 1234,temperatur: 19.8
条目到以mystream
为key
的流里面,从命令行里面返回了一个用于标识自动生成的项,1518951480106-0
。它第一个参数是key的名称,第二个参数是指定entry ID用于区别于在streams内部的编号。然而,在这个实例中,我们使用了*
,因为想生成一个新的ID。每个新的ID都将单调增加,所以跟简单的来说,每个新的项加入将会使用比已经加入的项都大的ID号。大多数时候自动生成ID都是你需要使用的,指定ID方式操作是非常罕见。我们将会在稍后更多讨论此话题。事实上每个Stream条目都有各自的ID,类似log文件中的行号,偏移字节数,能用于标识项。反过头来看我们的XADD
例子,在key和ID之后,后面就是我们想存入Stream条目的域-值对。
我们可以使用XLEN
获取当前Stream的长度。
> XLEN mystream
(integer) 1
Entry IDs
XADD
命令返回的项ID,明确标识了每个stream里面的条目,它包含两个部分组成
<millisecdsTime>-<sequenceNumber>
前面是产生entry的本地Redis节点时间戳(精确到毫秒),然而如果当前当前时间戳小于前面的entry节点,将使用前面的节点时间戳替换此值,所以如果本地时间戳滞后了也一样能维持单调增长。sn用于同一毫秒创建时做分辨,由于这个数值是64bit,可以近似当成无限制标识entries。
这种格式的IDs第一眼就看起来很奇怪,gentle的读者将会奇怪为什么时间会当成ID的一部分。原因是Redis streams支持对ID范围做查询。因为ID关系到条目的生成时间,提供了一种方便的按照时间来查询方式。我们将在接下来看到转化过的XRANGE
方式。
如果有某种原因我们需要自增IDs不与时间相关,取而代之使用外部系统ID,如前所述,XADD命令能使用显示ID,而不是通配符*来禁止触发自动生成机制。示例如下:
> XADD somestream 0-1 field value
0-1
> XADD somestream 0-2 foo bar
0-2
注意在这个情况下,最小的ID是0-1了,命令行将不会支持小于它的任何值了。
> XADD somestream 0-1 foo bar
(error) ERR The ID specified in XADD is equal or smaller than the target stream top item
这里也可以使用明显的ID替换掉milliseconds部分,而让sequence部分维持自增。
> XADD somestream 0-* baz qux
0-3
Getting data from Streams
现在我们最终能通过XADD添加条目到我们的stream。当追加数据到到stream非常明显了,然而,从stream里面提取出来数据就不是那么明显了。如果我们继续类比日志文件,一个明显的方式模仿UNIX的命令tail -f
,我们可以开始监听以便于获取什么新的消息被加入到了流中。注意不类似于Redis阻塞列表操作,这里提供了元素给单个客户端,以阻塞方式弹出风格类似于BLPOP
,使用流时我们想有众多消费者都能看到新消息被添加到流中(相同的方式多个tail -f
进程都能看到什么内容被加入到log中)。使用传统术语我们想流能扇出多个消息给多个客户端。
但是,这只是潜在的一种访问模式。我们可能也需要使用完全不同的方式查看一个流:不同于消息系统,而是按照时间序存储系统。在这种情况下可能它可能是非常有效的—去获取最新的数据被添加,通过时间跨度或者使用迭代器使用游标去依次遍历全部历史数据也是一种很常见的查询模式。很明显后者是非常有用的一种访问模式。
最后,我们从消费者的视角来看,我们可能想要访问一个流通过另一种模式,那就是,流里面的一个条目能被分配给多个消费者去处理这些数据,以便消费者组只能看到单个流到达的消息子集。这种方案,能将消息分配给不同的消费者来处理,而不是单个消费者处理全部的消息:每个消费者将会获取不同的消息来处理。这个基本上就是Kafka在消费者组里面做的工作。通过消费者组从一个流中获取数据是另外一种模式。
Redis流通过不同的命令支持以上三种查询模式。下章节将会演示这些方式,先从最简单、最直接的方介绍:范围来查询。
Querying by range: XRANGE and XREVRANGE
通过指定两个IDs来查询某个范围内的数据。将会返回[start,end]之间的元素,使用符号-
和+
标示最小和最大的ID。
> XRANGE mystream - +
1) 1) 1518951480106-0
2) 1) "sensor-id"
2) "1234"
3) "temperature"
4) "19.8"
2) 1) 1518951482479-0
2) 1) "sensor-id"
2) "9999"
3) "temperature"
4) "18.2"
每个条目返回值包含两个项目:ID和域-值对的列表。我们已经说过条目IDs和时间存在相关性,因为-
左边部分就是这个节点被创建出来当时的Unix时间戳(单位为毫秒)。这也就意味着我们可以通过时间为单位去查询范围内的数据。为了这么做,然而,我们需要忽略掉id中sequence部分:如果忽略,意味着将会读取sequence从0到最大值之间的数据。通过这种方式,我们就能查询两个Unix时间戳之间的数据。下面是通过两个时间戳查询信息:
> XRANGE mystream 1518951480106 1518951480107
1) 1) 1518951480106-0
2) 1) "sensor-id"
2) "1234"
3) "temperature"
4) "19.8"
我只有单条数据在这个范围,但是在真实数据集中,我可能查询跨度以小时为单位的数据,或者可能在两毫秒内有两条记录,也有可能返回信息非常的巨大。出于这个原因,XRAGE
支持使用COUNT
的选项。这样我们就能截取N个项。
> XRANGE mystream - + COUNT 2
1) 1) 1519073278252-0
2) 1) "foo"
2) "value_1"
2) 1) 1519073279157-0
2) 1) "foo"
2) "value_2"
如果我们想继续查询后续的元素可以使用(
前缀
> XRANGE mystream (1519073279157-0 + COUNT 2
1) 1) 1519073280281-0
2) 1) "foo"
2) "value_3"
2) 1) 1519073281432-0
2) 1) "foo"
2) "value_4"
XRANGE的复杂度,查询O(log(N)),返回M个元素O(M),这是较小的log时间复杂度,意味着每次迭代将会非常的快速。所以XRANGE事实上就是流的迭代器,不需要去使用XSCAN命令。
XREVRANGE相当于XRANG但是返回数据是反序的,下面这个练习就是从流中读取最后一个项。
> XREVRANGE mystream + - COUNT 1
1) 1) 1519073287312-0
2) 1) "foo"
2) "value_10"
注意这条XREVRANGE命令命令将获取从start到stop参数反序的内容。
Listening for new items with XREAD
当我们不想去通过范围去访问在流中批量的项时,通常我们订阅新消息到达了流。这个概念可能在Redis消息订阅发布里面出现过,你可以订阅一个频道或者是阻塞式的列表,当你等待一个key下获取新元素,但是这里消费流行为存在几点区别:
- 一个流能被多个消费者等待数据,每个新项,默认情况下将会派分给全部的在等待消息的消费者。这种方式不同于阻塞列表,阻塞列表每个消费者会获取不同的元素。然而,扇出给多消费者技术类似于Pub/Sub。
- 当Pub/Sub消息被fire或者forget将永远不会被存储,使用阻塞队列,当一个消息被客户端从list中poped,流的处理也是有本质的差异。全部消息都无限制的追加在流末尾(除非用户明显要求去删除项):不同的消费者将会知道什么是新消息从他自己记录的最后消费的ID。
- 流消费组提供了一个水平控制是Pub/Sub或者阻塞列表无法实现的,为相同的流配置不同的组,明确确认已处理项,可以查看被挂起的项,什么未处理项,每个客户端连续历史记录可见性,都只能看到私人的过去历史记录。
XREAD提供了监听新消息到达功能。它将会比XRANGE更加复杂,所以我们开始用最简单的方式,之后再将这个展开。
> XREAD COUNT 2 STREAMS mystream 0
1) 1) "mystream"
2) 1) 1) 1519073278252-0
2) 1) "foo"
2) "value_1"
2) 1) 1519073279157-0
2) 1) "foo"
2) "value_2"
上面是非阻塞方式的XREAD。注意COUNT选项不是强制的,在事实上只有STREAMS选项,这里制定了一系列key以及大于命令输入的ID号更大的消息。
在上面命令中我们写道STREAMS mystream 0
表示我们要获取从mystream全部的数据,条件为ID大于0-0
。我们可以看到命令将会返回key name,因为这个命令能支持输入多个key。我们也能写这样的语句STREAMS mystream otherstream 0 0
。这个选项都是最后一项。
不同于XREAD能一次性访问多个stream,我们能指定last ID我们拥有获取最新消息,在简单模式指令不会和XRANGE作比较。但是,有趣的地方是我们能通过XREAD指定BLOCK参数,轻松进入阻塞模式。
XREAD BLOCK 0 STREAMS mystream $
BLOCK选项设置了一个0毫秒的超时时间(意味着永不超时)。另外,我们制定了$
来替换0这个ID号。这就意味着刚刚存进来的最大的项的编号。所以我们只会在新消息到达的时候,才会处理。这个有些类似tail -f
。
XREAD只需要指定多个key就能监听多个流。如果请求同步服务,因为至少一个流的元素大于我们设定的ID,它将会返回结果。否则,这个命令将会阻塞直到返回第一个最新到达的数据(遵循指定的ID)。
类似于阻塞列表操作,阻塞流读是一种公平的客户端等待数据的模式,语义上最寻先进先出模式。第一个客户端阻塞在流上将会第一个由于数据到达被解锁。
XREAD除了COUNT/BLOCK没有其他的选项,它是一个基础的命令指定试图去消费一个或者多个的流。更多功能需要使用消费者组API,然而通过消费者组读取是使用到XREADGROUP,下章节将会涉及到。
Consumer groups
引用
- [1] Redis-Streams