技术开发 频道

Redis streams:作为一个纯数据结构会如何

  Redis 5中以“Streams”的名义引入的新Redis数据结构,在社区中引起了极大的反响。在这片文章中,笔者将试图解决这样一个问题:我开始怀疑,很多用户只是将Streams作为解决类Kafka(TM)用例的一种方法。实际上,该数据结构的设计也适用于生产者和消费者的消息传递上下文的工作,但是,如果你认为Redis Streams只是为了这个目的而存在的,那可想的太简单了。Streaming是一种非常棒的模式,可以在系统设计获得巨大成功时应用,不过Redis Streams与大多数Redis数据结构一样,更为通用,可用于建模十几种不同的、互不相关的问题。因此,在这篇文章中,我将把Streams作为一个纯数据结构来关注,完全忽略它的阻塞操作、用户组和所有消息传递部分。

Photo by rawpixel.com from Pexels

  Redis Streams

  如果你想记录一系列的结构化数据项,并且判断发现数据库最终还是被高估了,你可能会这样说:让我们以append only(只附加)模式打开一个文件,并将每一行记录为一个CSV(Comma Separated Value,逗号分隔值)项:

  (在append only模式下打开data.csv)

  time=1553096724033,cpu_temp=23.4,load=2.3

  time=1553096725029,cpu_temp=23.2,load=2.1

  看起来很简单,大家做了很多年,而且依然这样做:这似乎是一个固定的模式。但是那在内存中相当于什么呢?内存比append only文件更强大,可以自动删除CSV文件的如下限制:

  1、在这里进行范围查询很困难(效率低下)。

  2、冗余信息太多:每个条目的时间几乎相同,字段重复。同时,如果我想切换到一组不同的字段,删除它会降低格式的灵活性。

  3、Item偏移只是文件中的字节偏移量:如果我们更改文件结构,那么偏移量会是错误的,因此这里没有主ID的真正概念。条目基本上没有以某种方式单一处理。

  4.我无法删除条目,但是如果不能通过重写日志,我只能在没有垃圾收集功能的情况下将它们标记为无效。出于几个原因,日志重写一般会很糟糕,如果可以避免就很好了。

  尽管如此,CSV条目的日志在某种程度上还是非常棒的:没有固定的结构,字段可能会更改,生成起来很简单,而且毕竟非常紧凑。Redis Streams的理念是保留那些好东西,但要克服限制。其结果是一个与Redis排序集非常相似的混合数据结构:它们感觉像是一个基本的数据结构,但是为了获得上述的效果,在内部使用多个表示。

  Streams 101(如果你已了解Redis Streams的基础知识,可以跳过这部分)

  Redis Streams表示为由基数树链接在一起的delta压缩宏节点。其效果是能够以非常快的方式寻找随机条目,在有需要时获得范围,删除旧项以创建有上限的流,等等。然而,我们为程序员提供的接口非常类似于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是单调递增的且包含两个部分:<time> - <counter>。时间是毫秒级的,对于条目生成,计数器将会在相同毫秒内增加。

  因此,“append only CSV文件”概念之上的第一个新的抽象概念是,由于我们使用星号作为XADD的ID参数,所以我们可以从服务器免费获得条目ID。这样的ID不仅对指向流中的特定项很有用,还与将条目添加到流中的时间有关。事实上,使用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作为为range,我只需要使用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)来获取范围(取决于你想做什么),让我们看看为什么我说Redis Streams作为数据结构是如此强大。

  如果你想了解有关Redis Streams及其API的更多信息,可以下教程:https://redis.io/topics/streams-intro

  应用案例:网球运动员

  几天前,我和一个正在学习Redis的朋友一起制作了一个应用程序:一个用来跟踪当地网球场、球员和比赛的应用程序。在Redis中为球员建模的方式非常明显,球员是一个小对象,所以只需要一个Hash,关键名称为player:<id>。当你使用Redis作为主要工具,进一步对应用程序数据建模时,还需要一种方法来跟踪给定网球俱乐部的比赛。如果玩家1和玩家2玩了一场游戏,并且玩家1赢了,我们可以在Streams中写下以下条目:

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

  "1553254144387-0"

  通过这个简单的操作,我们有:

  1、比赛的唯一标识符:stream中的ID。

  2、无需创建对象即可识别一场比赛。

  3、范围查询,以便自由地随意翻阅比赛,或检查过去给定时刻的比赛。

  在Streams之前,我们往往需要创建一个按时间划分的有序集合(sorted set):有序集合的元素是比赛的ID,作为Hash值存在于不同的密钥中。这不仅仅意味这要做更多的工作,它还浪费了大量的内存,比你想象的还要多(见后文)。

  要说明的是,Redis Streams其实有点像是一个有序集合。在append only模式中,按时间键入,每个元素都是一个小Hash。简而言之,这是Redis建模环境中的一次革命。

  内存使用

  上述用例不仅仅是一个更加坚实的模式。stream解决方案的内存成本与传统方法相比,有很大的区别,后者对于每个对象都有一个有序集合 + Hash,这使某些事情会变得不可行,但对于前者完全没问题。

  下面是存储一百万场比赛数据的前后对比:

  Sorted Set + Hash 内存用例 = 220 MB (242 RSS)

  Stream 内存用例 = 16.8 MB (18.11 RSS)

  其中的差距已经超过了一个数量级(准确点说是13倍),这意味着,之前对于内存来说成本太高的用例现在是完全可行的。其中的神奇之处在于Redis Streams的表示:宏节点可以包含几个元素,这些元素以一种非常紧凑的方式编码在名为listpack的数据结构中。例如,即使整数是语义上的字符串,listpack也会注意以二进制形式编码整数。在此基础上,我们应用delta压缩和相同字段压缩。我们可以通过ID或时间来查找,因为这种宏节点是在基数树中链接的,而基数树的设计也是为了使用很少的内存。所有这些因素加在一起就带来了低内存使用量,但有趣的是,从语义上来说,用户看不到任何使Redis Streams有效的实现细节。

  现在,我们做一些简单的数学运算。如果我可以在大约18 MB的内存中存储100万个条目,那么我就可以在180 MB中存储1000万个,在1.8 GB中存储1亿个。只需要18 GB的内存,我就可以拥有10亿个项。

  时间序列

  需要注意的一件重要事情是,在我看来,上面我们使用一个Stream来表示网球比赛的用例与在时间序列中使用Redis Stream是非常不同的。从逻辑上讲,我们仍然在记录某种事件,但有一个基本区别是,在一种情况下,我们使用日志记录和创建条目来呈现对象。而在时间序列的情况下,我们只是计量外部发生的事情,它并不真正代表一个对象。你可能会觉得这种差异微不足道,但事实并非如此。对于Redis用户来说,重要的是要构建这样一个概念,即可以使用Redis Streams创建具有总序的小对象,并为这些对象分配ID。

  然而,即使是时间序列最基本的用例,在此处显然也是一个非常大的用例,因为在Streams之前,Redis对于这样的用例是无法抱有希望的。Streams的内存特性和灵活性,在加上有上限Stream的能力(参见XADD选项),是开发人员手中非常重要的工具。

  结论

  Redis Streams是灵活的,有很多用例,最后简短地总结一下这篇文章。对于许多读者来说,这可能已经很明显了,但在过去几个月与人交谈让我觉得Redis Streams和streaming用例之间存在强烈关联,就好像这一数据结构只擅长与此——当然事实并不是这样。

  原文作者:antirez 原文来源:http://antirez.com/news/128

1
相关文章