做为一个纯粹数据结构的 Redis Streams

来源:antirez程序员

翻译:Kevin (公众号:中间件小哥)redis

 

Redis 5 中引入了一个名为 Streams 的新的 Redis 数据结构,吸引了社区极大的兴趣。接下来,我会在社区里进行调查,同用户们谈谈他们在实际生产中的使用场景,而后写个博客记录一下。数据库

今天我想解决另外一个问题:我有点怀疑许多用户仅仅把Streams 做为解决相似 Kafka 所要解决的问题的一个手段。实际上,这个数据结构,在当初设计的时候,在生产者/消费者消息通讯的场景下,也是能够用起来的。并且我意识到 Streams 是很擅长这个场景的,用法也很简洁。Streaming 是一个很好的模式和“思惟模型”,在被用来设计系统时,能够得到巨大的成功。可是 Redis Streams 就像大多数 Redis 数据结构同样,是比较通用的结构,能够用来对许多不一样的问题进行建模。在本篇博文中,我将聚焦在做为纯粹数据结构的 Streams,彻底忽略其阻塞式的操做、消费者群组和全部和消息通信有关的部分。数据结构

做为 CSV 文件增强版的 Streamsapp

若是你要把一系列结构化的数据项记录下来,而且以为用数据库毕竟有点“杀鸡用牛刀”,那么你可能会说:让咱们以“仅追加”(append only)模式打开一个文件,而后把每一行做为 CSV(逗号分隔的值)格式记录下来:工具

(以 append only 模式打开 data.csv 文件)性能

time=1553096724033,cpu_temp=23.4,load=2.3
time=1553096725029,cpu_temp=23.2,load=2.1学习

看起来是很简单的,是吧,人们一直也是这么作的:这是一个一致的模式,若是你知道你在作什么的话。可是和这个(文件)模式对等的 in-memory(内存)模式是怎样的呢?内存比 append only 文件更强大,天然也就没有相似 CSV 文件的一些限制:编码

  1. 作范围查询比较难(效率低);
  2. 太多冗余信息:每条记录中的时间差很少是同样的,并且许多列都是重复的。同时,在你想切换到不一样的一组列时,若是移除这些冗余信息,这会使得格式的灵活性更低。
  3. 数据项的位移就是文件中的字节位移:若是咱们改变文件的结构,那么位移值就会是错的,因此实际上这里没有真正的 primary Id 的概念。
  4. 我不能移除这些数据条目,在没有 GC(垃圾收集)能力的状况下,只能将他们标记为“失效”,若是不重写 log(日志)的话。并且由于某些缘由,日志重写的性能不好,若是可以避免的话,就再好不过了。

从另一个角度看,这些 CSV 条目的日志也有好的方面:他们没有固定的结构,数据列能够变化,容易生成,并且毕竟其结构也是比较紧凑的。Redis Streams 的设计理念就是取长补短,其结果就是一个和 Redis Sorted sets 很是相似的混合型数据结构:他们看起来像是一个基础数据结构,为了达到这样一个效果,在底层他们有多种表现形式。spa

Streams 101

(你能够跳过这个部分,若是你已经了解 Redis Streams 的基础的话)

Redis Streams 由差分压缩(delta-compressed)的宏节点表示,这些节点经过基数树(radix tree)链接在一块儿。其效果就是,能够很是快的进行随机查找、按需获取范围、删除老的数据项,从而建立一个带上限的 stream,等等。同时,给程序员的接口和 CSV 文件是很是相似的:

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

从上面的例子咱们看到,XADD 命令自动产生和返回了记录 ID,记录 ID 是单调递增的,由 2 个部分组成:<时间>-<计数器>,时间以毫秒表示,对于在同一毫秒中产生的记录,计数器会递增。

以“只追加(append only)CSV 文件”的思想做为基础,咱们构建的第一个新的抽象是:既然咱们使用星号做为 XADD 命令的 ID 参数,从服务侧咱们就能够免费获得记录 ID。这个 ID 不只能够用来指示一个 stream 中的某一条数据记录,也关联了这条记录加入 stream 的时间。实际上,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 参数来限制查询结果的个数。一样的,也没必要详细指明完整的 ID 做为范围条件,能够只用 ID 的 Unix 毫秒时间戳部分,来获取给定时间范围内的元素。

> 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 了,详细的内容能够参考 Redis 文档。让咱们聚焦在其使用模式上:XADD 用来添加元素,XRANGE(也包括 XREAD)是用来获取范围内的元素(取决于你的目的),让咱们看下为何我把 Streams 称为一个如此强大的数据结构。

若是你想对 Streams 及其 API 了解更多的话,请必定看下这篇教程:https://redis.io/topics/streams-intro

 

网球选手

几天前我和一个最近正在学习 Redis 的朋友一块儿对一个应用进行建模,这个应用是用来记录本地的网球场、本地的选手和比赛的。用来对选手建模的方法是显而易见的:一个选手是一个小的对象,因此一个 hash 值加上选手:<id>的键就够了。当你使用 Redis 做为首要的应用数据建模的手段,你会立刻意识到,你须要一个方法来记录在一个给定网球俱乐部中举行的比赛。若是选手 1 和选手 2 打了一场比赛,选手 1 赢了,咱们能够在一个 stream 中记录以下:

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

经过这个简单的操做,咱们获得了:

  1. 一个惟一的比赛 ID:stream 中的 ID;
  2. 不须要为了标识一场比赛而建立一个对象;
  3. 免费的范围查询能够对比赛记录进行分页,也能够查看在过去一个给定时刻的比赛记录;

在 Streams 出现前,咱们须要建立一个按时间排序的 sorted set。sorted set 中的元素就是比赛的 ID,同时还须要做为 hash 值保存在一个不一样的 key 中。这不只意味着更多的工做,同时也带来了不可思议的内存浪费。还有更多的你能想到的状况(后面能够看到)。

目前,能够看到的一点是,Redis Streams 就是一种处于仅追加模式(append only)的 Sorted Set,以时间做为键,每一个元素是一个小的 hash 值。在对 Redis 进行建模的场景下,带来革命性的一点就是他的简洁。

内存使用

上述用例不只意味着一个从行为上看更为一致的模式。比起老的 Sorted set + hash 的方式,Stream 方案的内存开销是如此之低,以致于以前不具备可行性的东西,如今彻底是可行的。

如下数字是按以前的配置计算的、保存 100 万条比赛数据的开销:

Sorted Set + Hash 内存开销 = 220 MB (242 RSS)
Stream 内存开销 = 16.8 MB (18.11 RSS)

这超过了一个数量级的差异(准确的说是 13 倍的差异),并且这意味着那些以前在内存中开销太大的用例,如今彻底是可行的。神奇的地方就在于 Redis Streams:宏节点能够包含多个以 listpack 数据结构、很是紧凑的方式编码的元素。例如,即便整数在语义上是字符串,但 listpack 能够把他们编码为二进制形式。在这个基础上,咱们能够进行差分压缩和“相同列”的压缩。同时,由于宏节点在基数树(在设计上仅占用不多的内存)中连接在一块儿,咱们也能够经过 ID 和时间进行查询。全部这些加在一块儿,使得内存占用不多。有意思的是,在语义上,用户看不到任何使得 Streams 如此高效的实现细节。

如今,让咱们作一个简单的计算。若是我能够用 18MB 的内存存储 1 百万条记录,180MB 存 1 千万条,1.8GB 存 1 亿条记录。若是有 18GB 内存的话,能够存 10 亿条记录。

时间序列

依我看,咱们须要重点关注的是,上述咱们使用 Stream 表示网球比赛的用法,在语义上,同使用 Stream 处理一个时间序列是彻底不一样的。是的,逻辑上咱们仍然在记录某种事件,但一个重要的区别是,在一种场景下,咱们记录和建立记录条目来呈现对象;在时间序列场景下,咱们只是测量某些外部发生的事情,而这并不会表示成一个对象。你可能认为这个区别不重要,但其实否则。对于 Redis 用户,重要的一点是须要创建一个概念,Redis Streams 能够用来建立具备全序的小对象,每一个对象都有一个 ID。

时间序列是一个最基础的使用场景,显然,也是最重要的使用场景,但在 Streams 出现前,Redis 对这种场景是有些无能为力的。Streams 的内存特性和灵活性,加上带上限的 stream(capped stream)的能力(参考 XADD 命令的参数选项),在开发者的手中是一个很是有力的工具。

结论

Streams 是很是灵活的,并且有不少使用场景。好了,话很少说,上述的例子我想要传达的一个关键信息就是关于内存使用的分析,也许对于许多读者来讲这已经很明显了,可是最近几个月和人们的交谈给我一种感受,在 Streams 和 Streams 的使用场景之间有着很强的关联性,就好像这个数据结构只擅长这种场景同样,但其实不是这样的。:-)

 

多优质中间件技术资讯/原创/翻译文章/资料/干货,请关注“中间件小哥”公众号!

相关文章
相关标签/搜索