写点什么

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

  • 2019-12-09
  • 本文字数:3599 字

    阅读完需:约 12 分钟

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

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 非常类似的混合型数据结构:他们看起来像是一个基础数据结构,为了达到这样一个效果,在底层他们有多种表现形式。

Streams101

(如果你已经了解 Redis Streams 的基础的话,可以跳过这个部分)


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


> XADD mystream * cpu-temp23.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 mystream1553097561402-0 1553097561402-01) 1) "1553097561402-0"   2) 1) "cpu-temp"      2) "23.4"      3) "load"      4) "2.3"
复制代码


在这个例子中,为了标识单个元素,我使用了相同的 ID 作为范围查询的起止条件。但是,我也可以使用任何范围条件,加上一个 COUNT 参数来限制查询结果的个数。同样的,也不必详细指明完整的 ID 作为范围条件,可以只用 ID 的 Unix 毫秒时间戳部分,来获取给定时间范围内的元素。


> XRANGE mystream1553097560000 15530975700001) 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 倍的差别),而且这意味着那些之前在内存中开销太大的用例,现在完全是可行的。神奇的地方就在于 RedisStreams:宏节点可以包含多个以 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 的使用场景之间有着很强的关联性,就好像这个数据结构只擅长这种场景一样,但其实不是这样的。:-)


本文转载自公众号中间件小哥(ID:huawei_kevin)。


原文链接:


https://mp.weixin.qq.com/s/k4aOXSLHQALt_od68A33JQ


2019-12-09 13:432746

评论

发布
暂无评论
发现更多内容

Android 面试知识点

沃德

android 程序员 7月月更

Python|正则表达式

AXYZdong

Python 7月月更

Vue 灰度发布新功能的那些事

南城FE

Vue 前端 灰度发布 7月月更

如何参与开源项目 - 细说 GitHub 上的 PR 全过程

胡说云原生

GitHub 开源 pull request DevStream

OpenSergo 即将发布 v1alpha1,丰富全链路异构架构的服务治理能力

阿里巴巴中间件

阿里云 微服务 云原生 云原生开源 OpenSergo

基于华为云IOT设计智能称重系统(STM32)

DS小龙哥

7月月更

Qt|多个窗口共有一个提示框类

中国好公民st

qt 7月月更

【玩转 RT-Thread】 RT-Thread Studio —— 按键控制电机正反转、蜂鸣器

攻城狮杰森

OS 7月月更 RT-Thread

关于 Web Content-Security-Policy Directive 通过 meta 元素指定的一些测试用例

汪子熙

JavaScript 前端开发 CSP meta 7月月更

系统入门-Linux系统基础命令

Albert Edison

7月月更

当我们谈论不可变基础设施时,我们在谈论什么

阿里巴巴中间件

阿里云 容器 云原生 托管

自律,提升自制力原来也有方法

沃德

程序员 7月月更

简单介绍一下闭包及它的一些应用场景

是乃德也是Ned

7月月更

electron添加SQLite数据库

空城机

sqlite Electron 7月月更

java零基础入门-Java正则表达式

喵手

Java 7月月更

LeetCode-144. 二叉树的前序遍历(java)

bug菌

Leet Code 7月月更

【Python技能树共建】动态渲染页面爬取

梦想橡皮擦

Python 7月月更

Scala 基础 (六):面向对象(下篇)

百思不得小赵

scala 大数据 7月月更

Java 9 中的字符串(String)压缩的改进

HoneyMoose

Qt 实现容器的DELETE的方式

小肉球

qt 7月月更

Android自定义TextView实现高度和宽度,解决字体适配问题

芝麻粒儿

Android Studio TextView 7月月更

开发一个小程序商城需要多少钱?

CRMEB

iOS基础--属性(setter方法 、getter方法、点语法、@property)

NewBoy

前端 移动端 iOS 知识体系 7月月更

如何在博客中添加Aplayer音乐播放器

echeverra

前端

wallys/Qualcomm IPQ8072A networking SBC supports dual 10GbE, WiFi 6

wallys-wifi6

IPQ8072 IPQ9072a

【LeetCode】 解密消息Java题解

Albert

LeetCode 7月月更

盘点JS判断空对象的几大方法

猪痞恶霸

前端 js 7月月更

科普达人丨一文弄懂什么是云计算?

阿里云弹性计算

云计算 阿里云 虚拟化 神龙架构 IT资源利用

千人规模互联网公司研发效能成功之路

laofo

互联网 DevOps 研发效能 工程效率

《HarmonyOS实战—入门到开发,浅析原子化服务》

攻城狮杰森

操作系统 HarmonyOS 7月月更

Nginx 主机配置文件中如何配置能够支持 IPv4 和 IPv6

HoneyMoose

作为一个纯粹数据结构的Redis Streams_文化 & 方法_中间件小哥_InfoQ精选文章