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 类型的更多相关文章

  1. Redis Stream类型的使用

    一.背景 最近在看redis这方面的知识,发现在redis5中产生了一种新的数据类型Stream,它和kafka的设计有些类似,可以当作一个简单的消息队列来使用. 二.redis中Stream类型的特 ...

  2. 4、Redis中对List类型的操作命令

    写在前面的话:读书破万卷,编码如有神 -------------------------------------------------------------------- ------------ ...

  3. 3、Redis中对String类型的操作命令

    写在前面的话:读书破万卷,编码如有神 -------------------------------------------------------------------- ------------ ...

  4. Redis中的Stream数据类型作为消息队列的尝试

    Redis的List数据类型作为消息队列,已经比较合适了,但存在一些不足,比如只能独立消费,订阅发布又无法支持数据的持久化,相对前两者,Redis Stream作为消息队列的使用更为有优势.   相信 ...

  5. 详解Java 8中Stream类型的“懒”加载

    在进入正题之前,我们需要先引入Java 8中Stream类型的两个很重要的操作: 中间和终结操作(Intermediate and Terminal Operation) Stream类型有两种类型的 ...

  6. redis数据类型-散列类型

    Redis数据类型 散列类型 Redis是采用字典结构以键值对的形式存储数据的,而散列类型(hash)的键值也是一种字典结构,其存储了字段(field)和字段值的映射,但字段值只能是字符串,不支持其他 ...

  7. 尚硅谷redis学习4-数据类型

    redis的数据类型包括String,Hash(类似于JAVA里的map),List,Set,Zset(sorted Set) String(字符串) string是redis最基本的类型,你可以理解 ...

  8. 7、Redis中对ZSet类型的操作命令

    写在前面的话:读书破万卷,编码如有神 --------------------------------------------------------------------   ---------- ...

  9. 6、Redis中对Hash类型的操作命令

    写在前面的话:读书破万卷,编码如有神 -------------------------------------------------------------------- ------------ ...

随机推荐

  1. XTU | 人工智能入门复习总结

    写在前面 本文严禁转载,只限于学习交流. 课件分享在这里了. 还有人工智能标准化白皮书(2018版)也一并分享了. 绪论 人工智能的定义与发展 定义 一般解释:人工智能就是用 人工的方法在 **机器( ...

  2. tomcat重启应用和tomcat重启是两回事。热部署就是重启应用

    tomcat重启应用和tomcat重启是两回事.热部署就是重启应用 tomcat重启应用和tomcat重启是两回事.热部署就是重启应用 tomcat可以设置检测到新的class后重启该应用(不是重启t ...

  3. ASP.NET 5基础之中间件

    来源https://docs.asp.net/en/latest/fundamentals/middleware.html 一些可以整合进http请求管道的小的应用组件称做中间件.ASP.NET 5集 ...

  4. easyui numberbox precision属性

    //设置easyui numbox 最小值为0,保留2为小数 <input id="payPrice" type="text" name="pa ...

  5. #if 条件编译

    1.格式: #if constant-expression statements #elif constant-expression statements #else statements #endi ...

  6. ES6中的async函数

    一.概述 async 函数是 Generator 函数的语法糖 使用Generator 函数,依次读取两个文件代码如下 var fs = require('fs'); var readFile = f ...

  7. ES6里关于函数的拓展(一)

    一.形参默认值 Javascript函数有一个特别的地方,无论在函数定义中声明了多少形参,都可以传入任意数量的参数,也可以在定义函数时添加针对参数数量的处理逻辑,当已定义的形参无对应的传入参数时为其指 ...

  8. CentOS7.4 x64环境Percona-Server-5.6安装

    CentOS7.4 x64环境Percona-Server-5.6安装 下载MySQL $ cd /usr/local/src/ $ wget https://www.percona.com/down ...

  9. selenium执行报错:Process refused to die after 10 seconds, and couldn't taskkill it

    十二月 02, 2015 5:16:56 下午 org.openqa.selenium.os.ProcessUtils killWinProcess 警告: Process refused to di ...

  10. DevExpress控件之LayoutControl

    一.项目运行中不显示右键菜单 layoutControl1.AllowCustomization = false 二.控件超出容器后不显示滚动条 layoutControl1.AtuoScroll = ...