来源:antirez

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

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

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

作为 CSV 文件加强版的 Streams

如果你要把一系列结构化的数据项记录下来,并且觉得用数据库毕竟有点“杀鸡用牛刀”,那么你可能会说:让我们以“仅追加”(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 非常类似的混合型数据结构:他们看起来像是一个基础数据结构,为了达到这样一个效果,在底层他们有多种表现形式。

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 的使用场景之间有着很强的关联性,就好像这个数据结构只擅长这种场景一样,但其实不是这样的。:-)

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

作为一个纯粹数据结构的 Redis Streams的更多相关文章

  1. Redis Streams 介绍

    Stream是Redis 5.0版本引入的一个新的数据类型,它以更抽象的方式模拟日志数据结构,但日志仍然是完整的:就像一个日志文件,通常实现为以只附加模式打开的文件,Redis流主要是一个仅附加数据结 ...

  2. Redis Streams与Spark的完美结合

    来源:Redislabs 作者:Roshan Kumar 翻译:Kevin  (公众号:中间件小哥) 最近,我有幸在 Spark +AI 峰会上发表了题目为“Redis + Structured St ...

  3. ICCV2021 |重新思考人群中的计数和定位:一个纯粹基于点的框架

    ​ 论文:Rethinking Counting and Localization in Crowds:A Purely Point-Based Framework 代码:https://github ...

  4. [需求设计]从一个小需求感受Redis的独特魅力

    分享一个简单的小需求应该怎么设计实现以及有关Redis的使用 Redis在实际应用中使用的非常广泛,本篇文章就从一个简单的需求说起,为你讲述一个需求是如何从头到尾开始做的,又是如何一步步完善的.之前写 ...

  5. 从一个小需求感受Redis的独特魅力

    分享一个简单的小需求应该怎么设计实现以及有关Redis的使用 Redis在实际应用中使用的非常广泛,本篇文章就从一个简单的需求说起,为你讲述一个需求是如何从头到尾开始做的,又是如何一步步完善的. 需求 ...

  6. Redis数据结构(一)-Redis的数据存储及String类型的实现

    1 引言 Redis作为基于内存的非关系型的K-V数据库.因读写响应快速.原子操作.提供了多种数据类型String.List.Hash.Set.Sorted Set.在项目中有着广泛的使用,今天我们来 ...

  7. 从string.size()和string.length()聊到长度的问题和一个关于数据结构定义的技巧

    最近工作中要查看一下string的长度,然后忘了是哪个函数,所以去网上搜了一搜,决定把网上学的和其他的一些有关长度的东西在这里汇总一下, 然后就有了此帖. string 是从c语言的char数组的概念 ...

  8. 一个简单清晰的Redis操作类

    <?php /** * redis处理的二次封装 * */ class Redis{ private $_redis; private $_config; public function __c ...

  9. 一个萌新对redis的理解

    redis是用来保存一些常用的数据到内存,以加快数据读取,减少直接访问DB流量以降低DB压力.既然是放到内存的,那我们怎么样保证用户使用的时候不会出现与数据的差异呢,其实这叫“如何报证缓存数据的一致性 ...

随机推荐

  1. GUI引发的一场脑部大战|wine、wsl、mono、gtk、qt

    没写完不想写了,先发布吧,这就是一个引子. 在春天种下一颗种子---- GUI引发的一场脑部大战|wine.wsl.mono.gtk.qt 思路开拓了,方法一下子就来了 wine可以运行大部分Wind ...

  2. javascript预览本地图片

    <!doctype html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  3. 查看服务器内存、CPU、网络等占用情况的命令--汇总

    搭建测试环境过程中,需要对正在使用的aws服务器(实际这是一台虚拟出来的服务器),查看它在运行脚本,启动脚本时的内存,CPU,网络等使用情况 1.查看服务器cpu内核个数: -cat 每个物理cpu中 ...

  4. Mac版微信无法安装之始末

    前言 Mac版微信安装不了...纠结了一周时间 ̄□ ̄||... 今天终于可以登录了(虽然还是没有安装到电脑上,但可以使用了) 因为之前也查了很多,有人遇到,但是没有可以解决我这个问题的方法, 浪费了很 ...

  5. D3.js的v5版本入门教程(第十一章)——交互式操作

    D3.js的v5版本入门教程(第十一章) 与图形进行交互操作是很重要的!所谓的交互操作也就是为图形元素添加监听事件,比如说当你鼠标放在某个图形元素上面的时候,就会显示相应的文字,而当鼠标移开后,文字就 ...

  6. OpenFOAM——设置自定义非均匀场区域

    在使用OpenFOAM进行计算的时候,我们需要对计算域设置非均匀场,比如最典型的溃坝算例,在开始计算以前,我们需要首先设定某一区域的水的体积分数为1,就是下面这样的: 有可能我们在计算传热问题的时候, ...

  7. 基于Docker部署ETCD集群

    基于Docker部署ETCD集群 关于ETCD要不要使用TLS? 首先TLS的目的是为了鉴权为了防止别人任意的连接上你的etcd集群.其实意思就是说如果你要放到公网上的ETCD集群,并开放端口,我建议 ...

  8. java spring学习

    目的:为后面学习spring mvc ssm spring boot 打基础. 从单词就能看到有s,记录自学过程,感慨spring 一篇文章都写不完 介绍(来源百度百科): Spring是一个开源框架 ...

  9. MyBatis你只写了接口为啥就能执行SQL啊?

    一.静态代理 又是一年秋招季,很多小伙伴开始去大城市打拼.来大城市第一件事就是租房,免不了和中介打交道,因为很多房东很忙,你根本找不到他.从这个场景中就可以抽象出来代理模式: ISubject:被访问 ...

  10. 集合类 collection接口 LinkedList

    LinkedList 是另外一种重要的数据结构形式, 底层是使用了双向链表数据结构, 特点: 查询速度慢,增删快. 继承关系如下: 可以发现,LinkedList同时实现了Quene和Deque接口. ...