【译】Redis喜提新数据结构:Redis Streams

本文是Redis做者antirez的一篇博客node

原文地址:antirez.com/news/128程序员

咱们在Redis5版本迎来了一个新的数据结构,它的名字叫作"Streams"。(撒花)Streams一经推出,就引发了社区中各位大佬的关注。因此我决定过一段时间作一个社区调查,讨论一下它的使用场景,并会在博客中将结果记录下来(是Redis做者的博客)。今天我想聊的是另外一个问题:我怀疑有不少用户认为Streams的使用场景是和Kafka同样的。实际上,这个数据结构的设计背景也是消息的生产和消费,但你应该认为Redis Streams只是更擅长作这样的事情。流是一种很好的模型和"心理模型",它能帮助咱们更好的设计系统,可是Redis Streams像其余Redis数据结构同样,它更加通用,能够用来处理更多不一样的问题。因此这篇博客咱们会重点关注Redis Streams做为一种数据结构有哪些特性,而彻底忽略它的阻塞操做、消费群和全部消息相关的内容。redis

Streams是steroids上的CSV文件

若是你想记录一系列的结构化数据,而且肯定数据库是足够大的,你可能会说:咱们以追加写入的方式打开一个文件,每一行记录是一个CSV数据项:数据库

time=1553096724033,cpu_temp=23.4,load=2.3 time=1553096725029,cpu_temp=23.2,load=2.1bash

这看起来很简单,而后人们一直这样作了好多年,而且一直持续着:若是你知道你在作什么,那么这将成为一种固定的模式。若是一样的事情发生在内存中会怎样呢?内存的顺序写入能力更强,而且会自动移除掉CSV文件的一些限制:服务器

  1. 很难批量查询
  2. 太多的冗余信息:每一个条目的时间几乎相同,字段也相同。可是移除字段会下降灵活性,就不能再增长别的字段了
  3. 每一个条目的偏移量都是它在文件中的字节偏移量,而若是咱们修改了文件结构,那么这些偏移量就会失效。因此这里缺乏一个惟一标识的ID。
  4. 不能删除条目,只能标记无效。若是不重写日志的话,又没有垃圾回收,重写日志常常会由于各类缘由出错,因此最好不要重写。

不过使用这样的CSV条目也有一些好处:没有固定格式,字段能够改变,生成比较容易,并且存储格式比较紧凑。咱们保留了其优势,去掉了限制,因而设计出了像Redis Sorted Set这样的混合数据结构——Redis Streams。他们看起来像基本数据结构同样,可是为了获得这样的效果,内部是有多种表现形式的。数据结构

Streams 101(就是Streams基础部分)

Redis Streams是一种经过基数树链接的增量压缩的宏节点。(好难理解的概念,把原话贴出来:Redis Streams are represented as delta-compressed macro nodes that are linked together by a radix tree)。它的做用是,快速查找一个随机项,获取范围值,删除旧值来建立一个有大小上限的流。对程序员来讲,咱们的接口和CSV文件很类似:app

> XADD mystream * cpu-temp 23.4 load 2.3
"1553097561402-0"
> XADD mystream * cpu-temp 23.2 load 2.1
"1553097568315-0"
复制代码

经过这个例子能够看到,XADD命令自动生成并返回了一个entry ID。它是单调递增的,而且有两部分组成,<时间>-<数量>,时间是毫秒级,而数量则是同一毫秒生成的entry数量递增。工具

因此第一个从上面所说的"追加写入CSV"文件抽象出来概念就是,若是用星号做为XADD命令的ID参数,就从服务器获取了一个entry ID。这个ID不只仅是entry的惟一标识,也和entry加入流的时间有关。XRANGE命令能够批量获取或获取单个数据项。学习

> XRANGE mystream 1553097561402-0 1553097561402-0
1) 1) "1553097561402-0"
   2) 1) "cpu-temp"
      2) "23.4"
      3) "load"
      4) "2.3"
复制代码

在这个例子中,为了获得单个数据项,我用了相同的ID做为起始和结束值。然而我能够获取任意范围的数据项,而且用COUNT参数限制结果的数量。我也能够将起止参数都设置为时间戳,获取一段时间内的数据项。

> XRANGE mystream 1553097560000 1553097570000
1) 1) "1553097561402-0"
   2) 1) "cpu-temp"
      2) "23.4"
      3) "load"
      4) "2.3"
2) 1) "1553097568315-0"
   2) 1) "cpu-temp"
      2) "23.2"
      3) "load"
      4) "2.1"
复制代码

篇幅缘由,咱们再也不展现更多的Streams API了。咱们有相关的文档,感兴趣的同窗能够去阅读。目前为止,咱们只须要关注基本使用方法:XADD用来增长数据,XRANGE(或XREAD)用来读取数据。咱们来看一下我为何说Streams是一个强大的数据结构。

网球运动员

前几天我和一个最近在学习Redis的朋友一块儿建模一个应用程序:这是一个用来追踪当地网球场、球员和比赛的app。很明显,球员是一个小的模型,在Redis中只须要用一个hash就足够了,key的形式能够是player:<id>。当你进一步使用Redis建模时,就会意识到你须要去追踪指定网球俱乐部的一场比赛。若是球员1和球员2打了一场比赛,球员1获胜。那么咱们能够这样来记录:

> XADD club:1234.matches * player-a 1 player-b 2 winner 1
"1553254144387-0"
复制代码

经过这样简单的操做,咱们就能够得到以下的信息:

  1. 一场比赛的惟一标识:流里的ID
  2. 不须要建立一个表示比赛的对象
  3. 分页查询比赛状况,或者查看某场比赛是否在指定时间就进行

在Streams出现以前,咱们须要建立一个Sorted Set,分数是时间。Sorted Set的元素是比赛的ID(存在一个Hash里)。这不只是增长了工做量,并且还形成了更多的内存浪费,比你想象的要多得多。

如今看起来Streams像是一个追加模式的,以时间为分数,元素是小型Hash的Sorted Set。简而言之,这是Rediscover建模环境中的一次革命。

内存使用状况

上面的例子不只仅是固化模式的问题,相比旧有的Sorted Set+ Hash的模式,Streams对内存的节省作了很好的优化,然而这一点是不容易被发现的。

假设咱们要记录100万场比赛,

Sorted Set + Hash的内存使用量是220MB(242RSS)

Stream的内存使用量是16.8MB(18.11RSS)

这不只仅是一个数量级的差别(其实是13倍的差别),这意味着咱们旧有的模式实在是太浪费了,而新的模式是完美可行的。Redis Streams还有其余神奇的地方:宏节点能够包括多个元素,它们使用叫作listpack的数据进行编码。listpacks会对二进制形式的整数进行编码,即时它是语义字符串。最重要的是,咱们使用了增量压缩和相同字段压缩。咱们能够经过ID或时间进行查询,由于宏节点是用基数树链接的。基数树叶被设计为使用不多的内存。全部的事情都使用极少的内存,但有趣的是,用户并不能从语义上看到使Streams更加高效的实现细节。

如今咱们来作一个简单的计算,若是我保存了100万个entry,使用了18MB内存,那么1000万个就是180MB,1亿个使用1.8GB,保存10亿数据也只使用18GB内存。

时间序列

有一个比较重要的事情须要注意,在我看来,上面咱们用来记录网球比赛的例子与把Redis Streams做为一个时间序列来使用很是不一样。没错,逻辑上咱们仍然是记录一类事件,但本质上的区别是记录日志和建立一个entry并存入对象的不一样。在使用时间序列时,咱们只是记录一个外部事件,而不须要真的展现一个对象。你可能认为这个区别不重要,但事实不是这样。对Redis用户来讲很重要的是,若是须要保存一系列有序的对象,而且给每一个对象赋一个ID,那么就须要使用Redis Streams。

然而即时是一个简单的时间序列,也是一个很大的用例,由于在Streams出现以前,Redis在面对这种用例时使人有些绝望。一个节省内存,而且灵活的流,对开发者来讲是一个重要的工具。

结论

Streams很是灵活而且有不少使用场景,我想尽可能用简短的语言,以确保上面的例子和内存分析更加通俗易懂。也许大多数读者已经搞懂了。不过在上个月我和别人交流时感受到Streams和流式处理仍是有着很强的关联。就像这个数据结构只能用来处理流同样,事实并不是如此。

相关文章
相关标签/搜索