探究 Redis 4 的 stream 类型

摘要:
别担心,这是一个好消息:Redis推出了一种新的数据类型stream和相应的命令,将于今年年底正式发布到4.x版本。当然,这并不意味着Redis将提供Kafkastream特性的替代方案。它们仍然是两个截然不同的东西。Redis的流特性旨在填补PubSub和Blockedlist机制之间的空白,解决两者无法解决的问题。用法遵循其他数据类型的约定。操作流类型键的命令以X开头。流没有特殊的删除命令。Redis将每个流实现为前缀,ID值作为关键字
2

10 月初,Redis 搞了个大新闻。别紧张,是个好消息:Redis 引入了名为 stream 的新数据类型和对应的命令,大概会在年底正式发布到 4.x 版本中。像引入新数据类型这样的变化在 Redis 的发展历史上非常罕见,所以称之为大新闻一点也不为过。至少很多介绍 Redis 的资料要跟着修订了。

背景

按作者的介绍,stream 类型的想法深受 Kafka 的 stream 概念的影响,所以顺理成章沿用了这个名字。当然这并不意味 Redis 将提供 Kafka stream 特性的替代品,它俩依旧是两种泾渭分明的东西。Redis 的 stream 特性旨在填补 PubSub 和 Blocked list 机制间的空缺,解决这两者不能解决的问题。

Redis 的 PubSub 可以用来实现简单的订阅机制。一个或多个 client 向 Redis 订阅特定的频道,当某个 client 向这个频道发布消息时,Redis 会把消息发送给订阅该频道的 client。需要注意的是,Redis 只负责转发消息,并不保证订阅的 client 是否真正收到了消息,比如 client 可能正好挂掉了或者中间出了点网络问题。在某些情况下,这种简单的订阅机制就够用了;但在某些情况下,我们需要确保消息已经发布出去,PubSub 就不能满足要求。

一个替代的方案是采用 BLPOP 等命令,也即前文提到的 Blocked list。client 调用 BLPOP(或其他类似的命令),阻塞在特定的频道上。如果有 client 发布消息(在这里,就是 rpush 新的值),被阻塞的 client 就会结束阻塞,得到新 rpush 进来的值。如果 Redis 没法把新消息发送给 client,那么这个消息会留在频道里。当 client 下次重新调用 BLPOP 时,就能拿回这个消息。这个方案听起来不错,至少它解决了确保消息发布的问题。但你可能也想到了,能收到特定频道的消息的只有一个 client,因为只要某个 client 接收了消息,消息就不再存在于频道当中了。而 PubSub 是支持一对多发送消息的。另一个问题是,每个 client 只能去获取最新的消息,对于复杂的操作,BLPOP 等命令便无能为力了。

stream 就是为了解决以上问题才提出来的。

用法

遵循其他数据类型的惯例,操作 stream 类型的键的命令都以 X 开头。
(由于 stream 特性尚未正式发布,且部分特性还处于 TODO 状态,下面内容肯定会有所变更。如果有改动,我会修订这部分的内容)

添加操作
XADD key [MAXLEN [~] <count>] <ID or *> [field value] [field value] ...

stream 跟 hash 一样,有 subkey 的概念。上面命令里的 ID 就是指 subkey。一般情况下,你不需要指定 ID,仅需提供 * 来让 Redis 生成一个 ID。Redis 生成的 ID 格式如下:$ms-$seq。其中 $ms 指当前的 13 位毫秒时间戳,$seq 指给定 key 在当前毫秒时间戳下的序列号(从 0 开始),中间以 - 隔开。早前用的分隔符是 .,后来考虑到 xx.yy 这种形式太容易错当作浮点数了,所以改用 -。如果 Redis 生成 ID 的时候,当前毫秒时间戳跟上一个 ID 的时间戳一样,它会把序列号加一。假使服务器发生时间回拨的情况,Redis 会沿用上一个 ID 的时间戳,只是把序列号加一。实际上这种生成 ID 的机制并非为了记录创建的时间,仅仅用于生成递增的 ID。你也可以在调用时指定自己生成的 ID。

[field value] ... 这部分指定的是 stream key 对应 ID 的值。每个 ID 带的 field 可以不同。

取长度操作
XLEN key

返回长度,就是这样。

读取操作
XRANGE key start end [COUNT <n>]

XRANGE 返回某个 stream 给定范围内的 ID 所对应的值。你可以通过 COUNT 指定返回的值的最大数目。
举个例子,像这样创建两个 ID:

127.0.0.1:6379> xadd test * apple 1
1507383725597-0
127.0.0.1:6379> xadd test * binana 2
1507383735965-0

如下的 XRANGE 操作能够返回这两个 ID 的值。

127.0.0.1:6379> xrange test 1507383725597-0 1507383735965-0
1) 1) 1507383725597-0
   2) 1) "apple"
      2) "1"
2) 1) 1507383735965-0
   2) 1) "binana"
      2) "2"

大多数情况下,你用到的是 - 和 + 这两个特殊 ID 值,像这样:xrange test 1507383725597-0 +。前者表示 ID 范围的起始位置,后者表示 ID 范围的末尾位置。

XREAD [BLOCK <milliseconds>] [COUNT <count>] [GROUP <groupname> <ttl>]
      [RETRY <milliseconds> <ttl>] STREAMS key_1 key_2 ... key_N
      ID_1 ID_2 ... ID_N

如果想同时读取多个 stream 的值,需要用到 XREAD。XREAD 能够返回给定多个 stream 的某个 起始ID 之后的数据。我加粗了之后两个字,因为跟 XRANGE 不同,XREAD 不返回 起始ID 的值。你可以通过 COUNT 指定各个 stream 返回的值的最大数目。

XREAD 的阻塞是可选的,你可以通过 BLOCK 参数去指定允许阻塞的时间。如果不指定,表示不阻塞,立刻返回 nil。注意这一点跟 BLPOP 不同,BLPOP 一类的命令,默认是永久阻塞的。

XREAD 主要的参数是 STREAMS 后面的 key 和 起始ID 列表。key 和 起始ID 需要是一一对应的,有多少个 key 就要指定多少个 起始ID。跟 XRANGE 一样,起始ID 也可以是 - 和 + 这样的特殊值。注意由于 - 表示 ID 范围的起始位置,而不是第一个 ID,所以用 - 可以获取第一个 ID 的值。除此之外,起始ID 还可以是 $,表示获取命令执行之后的新增 ID 的值。显然,$ 只有跟 BLOCK 一起用才有意义。

RETRY/GROUP:尚未实现 TODO。

删除操作

stream 不支持“改”操作,所以“增删查改”还剩个“删”没讲。stream 没有专门的删命令。还记得介绍 XADD 时展示的 MAXLEN 参数吗?在 XADD 命令添加了新的 ID 之后,如果命令指定的 MAXLEN 超过了当前 stream 包含的 ID 的个数,Redis 会删除多出来的部分。

重新贴下 MAXLEN 的格式:XADD MAXLEN [~] <count> ...。count 决定了 MAXLEN 的值。如果 MAXLEN 和 count 之间没有插入 ~,表示精确地保留 count 个 ID;如果插入了 ~,表示保留大约 count 个 ID。我会在“实现”这一节解释所谓的“精确”和“大约”的区别。

用途

stream 很大程度上类似于 Blocked list,但是它的操作更加自由,不再受限于只能读取最新的值,也不再拘束于只能让单个 client 读取值。跟 PubSub 相比,stream 允许 client 重新获取发布过的值,提供了更强的保障。

实现

Redis 把每个 stream 实现成以 ID 的值为 key 的前缀树,外加 length(当前的 ID 数)等元数据。考虑到默认生成的 ID 是毫秒时间戳+序列号,采用前缀树的形式可以节省下大量的空间。毕竟差几千毫秒的两个 ID,也会有前九位是完全相同的。另外前缀树还允许随机访问某个起始ID。

不过并非每个 ID 都是独占一个节点。每当插入一个新的 ID 时,Redis 会先访问前缀树的最大的节点(毕竟 ID 是递增的),如果这个节点不大于 STREAM_BYTES_PER_LISTPACK(2048字节),新的 ID 会被插入到这个节点里面;否则才会创建新的节点。在查找一个 ID 时,Redis 会查找最后一个比该 ID 小的节点,然后从该节点往后遍历,直到找到该 ID 为止。在我看来,一个节点里包含多个 ID 的设计,有利于 ID 遍历的操作。这种设计避免了在遍历时频繁访问新的节点,更好地利用了 CPU 的本地缓存。

每个节点具有这样的结构:

+--------------+---------+---------+--/--+---------+
| master_entry | entry_1 | entry_2 | ... | entry_N |
+--------------+---------+---------+--/--+---------+

其中
master_entry:
+-------+---------+------------+---------+--/--+---------+---------+
| count | deleted | num-fields | field_1 | field_2 | ... | field_N |
+-------+---------+------------+---------+--/--+---------+---------+

entry_x(SAMEFIELDS):
+-----+--------+-------+-/-+-------+
|flags|entry-id|value-1|...|
+-----+--------+-------+-/-+-------+
或者
+-----+--------+----------+-------+-------+-/-+---+
|flags|entry-id|num-fields|field_1|value_1|...|
+-----+--------+----------+-------+-------+-/-+---+

当节点被创建时,会以第一个插入的 ID 初始化 master_entry 的值。显然,count 的初始值是 1,deleted 的初始值是 0,num-fields 等于该 ID 对应的 field 数目,后面的多个 field 则是该 ID 对应的 field 数。在插入 master_entry 之后,还会新增一个 entry 来记录额外的 field 和每个 field 对应的 value。这个新增的 entry 的 entry-id 取 ID 跟前缀树节点的 key 的差。第一个 ID 的 entry-id 为 0,因为当前节点的 key 就是这个 ID,两者不存在差异。之后每插入一个新的 ID,都会更新 master_entry 的 count 数,并插入对应的 entry。当然插入新 ID 的同时也不忘更新 length 等元数据。

前面提到,每个 ID 带的 field 可以不同。但是在实际的使用中,每个 ID 带的 field 基本是相同的。所以 Redis 做了个优化:如果新增的 ID 的 field 跟 master_entry 完全一样,entry 里面会设置一个名为 SAMEFIELDS 的 flags,并仅记录 value 的值。除非新增 ID 的 field 跟 master_entry 有些不同,entry 里面才会记录新增 ID 的所有 field 和对应的 value。

最后说一下删除操作。由于

  1. stream 的删除操作,只支持保留特定数目的 ID 数
  2. stream 会记录全部的 ID 数(length)
  3. stream 的数据结构大体上是一个前缀树,前缀树的每个节点包含 count 个 ID

所以删除操作,就是

  1. 从前往后遍历,减去每个前缀树节点的 count,直到 length 等于 XADD 指定的 MAXLEN,或者减去下一个节点后剩下的 length 会小于 MAXLEN
  2. 如果减去某个节点后,剩下的 length 小于 MAXLEN,Redis 会遍历该节点,设置若干个 entry 的 flags 为 DELETED 直到 length 等于 MAXLEN,更新 count 和 deleted 两个域。

如果 XADD 命令指定的 MAXLEN 包含 ~,则表示大约保留 MAXLEN 个 ID。在这种情况下,Redis 只会完成上面的第一步。换句话说,选择“大约”能省下对某个节点进行遍历的时间。

免责声明:文章转载自《探究 Redis 4 的 stream 类型》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇docker中安装anaconda+ jupyter(远程访问)+tensorflowRT1052 BootLoader总结_后续Bin合并下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Redis热点数据高频访问问题以及解决方案

一、热点数据的存放 场景: 数据库中有2000w数据,而redis中只有100w数据,如何保证redis中存放的都是热点数据? 方案:限定redis占用的内存,redis会根据自身数据淘汰策略,留下热数据到内存。所以可以计算100w数据大约占用的内存, 然后设置一下redis内存限制即可,并将淘汰策略设置为allkeys-lru或者volatile-lru...

Redis 如何存储上亿级别的用户状态?

作者:铂赛东链接:https://www.jianshu.com/p/ee79ae681b74 1 前段时间,在网上看到一道面试题: 如何用redis存储统计1亿用户一年的登陆情况,并快速检索任意时间窗口内的活跃用户数量。 觉得很有意思,就仔细想了下 。并做了一系列实验,自己模拟了下 。还是有点收获的,现整理下来。和大家一起分享。 Redis是一个内存数...

Server-Sent Events入门

前言 SSE(Server-Sent Events)是一种服务器消息推送技术,是HTML5标准协议中的一部分,类似WebSocket,不同在于WebSocket可以双向通信,SSE只能服务器向浏览器发送消息。具体的规范可以查看 MDN。 简单使用 客户端,注意IE浏览器可能不支持 <script> // 初始化, 参数为url...

HOOK技术的一些简单总结

好久没写博客了, 一个月一篇还是要尽量保证,今天谈下Hook技术。 在Window平台上开发任何稍微底层一点的东西,基本上都是Hook满天飞, 普通应用程序如此,安全软件更是如此, 这里简单记录一些常用的Hook技术。 SetWindowsHookEx 基本上做Windows开发都知道这个API, 它给我们提供了一个拦截系统事件和消息的机会, 并且它可...

常用rides命令

rides使用步骤 1.源代码构建安装 1.下载,Linux下命令wget http://redis.io/download下载redis的包 2.解归档Linux下命令 tar -xvf redis-4.0.11 3.进入解归档后的文件夹cd redis-4.0.11 4.构建安装Linux下命令make && make ins...

VS关于生成exe文件图片不显示的解决以及两种简单的打包方法

当辛辛苦苦写好一段带有图形界面的代码而exe无法进行图形输出,显然是一件十分苦恼的事情,下面就提供一种此类问题的解决方案,并且简单介绍一种最简单的打包方法以及vs的一种打包 问题描述 当代码在vs上运行时可以得到正常的图形输出 然而而通过exe文件打开却是一片漆黑 问题分析 既然vs上可以正常运行,说明代码是没有问题的,想要解决问题显然下一步需要从ex...