PRF:20171003 Streams a new general purpose data structure in Redis.md

部分,至 33 行。
This commit is contained in:
Xingyu.Wang 2018-07-21 01:35:56 +08:00
parent 5f615bf4c5
commit 528649f6b9

View File

@ -1,29 +1,34 @@
streams一个新的 Redis 通用数据结构
==================================
直到几个月以前对于我来说在消息传递的环境中streams 只是一个有趣且相对简单的概念。在 Kafka 流行这个概念之后,我主要研究它们在 Disque 实例中的用途。Disque 是一个将会转化为 Redis 4.2 的模块的消息队列。后来我发现 Disque 全都是 AP 消息,它将在不需要客户端过多参与的情况下实现容错和保证送达,因此,我认为 streams 的概念在那种情况下并不适用。
直到几个月以前对于我来说在消息传递的环境中streams 只是一个有趣且相对简单的概念。在 Kafka 流行这个概念之后,我主要研究它们在 Disque 实例中的用途。Disque 是一个将会转变为 Redis 4.2 模块的消息队列。后来我发现 Disque 全都是 AP 消息,它将在不需要客户端过多参与的情况下实现容错和保证送达,因此,我认为 streams 的概念在那种情况下并不适用。
但是,在 Redis 中有一个问题,那就是缺省情况下导出数据结构并不轻松。它在 Redis 列表、排序集和发布/订阅Pub/Sub能力上有某些缺陷。你可以合适地使用这些工具去模拟一个消息或事件的序列而有所权衡。排序集是大量耗费内存的不能自然的模拟一次又一次的相同消息的传递客户端不能阻塞新消息。因为一个排序集并不是一个序列化的数据结构它是一个根据它们量的变化而移动的元素集它不是很像时间系列一样的东西。列表有另外的问题它在某些特定的用例中产生类似的适用性问题你无法浏览列表中部是什么因为在那种情况下访问时间是线性的。此外没有任何的指定输出功能列表上的阻塞操作仅为单个客户端提供单个元素。列表中没有固定的元素标识也就是说不能指定从哪个元素开始给我提供内容。对于一到多的工作负载这里有发布/订阅,它在大多数情况下是非常好的,但是,对于某些不想“即发即弃”的东西:保留一个历史是很重要的,而不是断开之后重新获得消息,也因为某些消息列表,像时间系列,在用范围查询浏览时,是非常重要的:在这 10 秒范围内我的温度读数是多少?
然而同时,在 Redis 中有一个问题,那就是缺省情况下导出数据结构并不轻松。它在 Redis 列表、排序集和发布/订阅Pub/Sub能力之间有某些缺陷。你可以权衡使用这些工具去模拟一个消息或事件的序列。
排序集是大量耗费内存的,不能自然的模拟一次又一次的相同消息的传递,客户端不能阻塞新消息。因为一个排序集并不是一个序列化的数据结构,它是一个元素可以根据它们量的变化而移动的集合:它不是很像时间系列一样的东西。
列表有另外的问题,它在某些特定的用例中产生类似的适用性问题:你无法浏览列表中部是什么,因为在那种情况下,访问时间是线性的。此外,没有任何的指定输出功能,列表上的阻塞操作仅为单个客户端提供单个元素。列表中没有固定的元素标识,也就是说,不能指定从哪个元素开始给我提供内容。
对于一到多的工作任务,有发布/订阅机制,它在大多数情况下是非常好的,但是,对于某些不想“即发即弃”的东西:保留一个历史是很重要的,而不是断开之后重新获得消息,也因为某些消息列表,像时间系列,用范围查询浏览是非常重要的:在这 10 秒范围内我的温度读数是多少?
这有一种方法可以尝试处理上面的问题,我计划对排序集进行通用化,并列入一个唯一的、更灵活的数据结构,然而,我的设计尝试最终以生成一个比当前的数据结构更加矫揉造作的结果而结束。一个关于 Redis 数据结构导出的更好的想法是让它更像天然的计算机科学的数据结构而不是“Salvatore 发明的 API”。因此在最后我停止了我的尝试并且说“ok这是我们目前能提供的”或许我将为发布/订阅增加一些历史信息,或者将来对列表访问增加一些更灵活的方式。然而,每次在会议上有用户对我说“你如何在 Redis 中模拟时间系列” 或者类似的问题时,我的脸就绿了。
### 起源
在将 Redis 4.0 中的模块介绍完之后用户开始去看他们自己怎么去修复这些问题。他们之一Timothy Downs通过 IRC 写信给我:
Redis 4.0 中引入模块之后,用户开始去看他们自己怎么去修复这些问题。他们中的一个Timothy Downs通过 IRC 写信给我:
<forkfork> 这个模块,我计划去增加一个事务日志式的数据类型 - 这意味着大量的订阅者可以在没有大量的内存增加的情况下做一些像发布/订阅那样的事情
<forkfork> 订阅者保持他在消息队列中的位置,而不是在 Redis 上维护每个客户和复制消息的每个订阅者
\<forkfork> 这个模块,我计划去增加一个事务日志式的数据类型 —— 这意味着大量的订阅者可以在没有大量增加 redis 内存使用的情况下做一些像发布/订阅那样的事情
\<forkfork> 订阅者保持它在消息队列中的位置,而不是让 Redis 必须维护每个消费者的位置和为每个订阅者复制消息
这激发了我的想像力。我想了几天,并且意识到这可能是我们立刻同时解决上面的问题的契机。我需要去重新想像 “日志” 的概念是什么。它是个基本的编程元素,每个人都使用到它,因为它是非常简单地在追加模式中打开一个文件并以一定的格式写入数据,数据结构必须是抽象的。然而 Redis ,它们在内存中,并且我们使用 RAM 并不是因为我们懒,但是,因为使用一些指针,我们可以概念化数据结构并让他们抽象,并允许他们去摆脱明显的限制。对于实例,正常的日志有几个问题:偏移不是逻辑的,但是,它是一个真实的字节偏移,如果你想逻辑偏移是什么,那是与条目插入的时间相关的,我们有范围查询可用。同样的,一个日志通常很难收集:在一个只追加的数据结构中怎么去删除旧的元素?好吧,在我们理想的日志中,我们只是说,我想要最大的条目,而旧的元素一个也不要,等等。
这激发了我的想像力。我想了几天,并且意识到这可能是我们立刻同时解决上面所有问题的契机。我需要去重新构想 “日志” 的概念是什么。它是个基本的编程元素,每个人都使用过它,因为它只是简单地以追加模式打开一个文件,并以一定的格式写入数据。然而 Redis 数据结构必须是抽象的。它们在内存中,并且我们使用内存并不是因为我们懒,而是因为使用一些指针,我们可以概念化数据结构并把它们抽象,以允许它们摆脱明显的限制。对于正常的例子来说,日志有几个问题:偏移不是逻辑的,而是真实的字节偏移,如果你想要与条目插入的时间相关的逻辑偏移,我们有范围查询可用。同样的,日志通常很难进行垃圾收集:在一个只追加的数据结构中怎么去删除旧的元素?好吧,在我们理想的日志中,我们只是说,我想要编号最大的那个条目,而旧的元素一个也不要,等等。
当我从 Timothy 的想法,去尝试着写一个规范的时候,我使用了 radix 树去实现,它是用于 Redis 集群的去优化它内部的某些部分。这为实现一个有效的空间日志提供了基础。它在对数的时间logarithmic time内得到范围是仍然可访问的。同时我开始去读关于 Kafka 流,去得到另外的创意,它也非常适合我的设计,并且产生了一个 Kafka 客户组的概念,并且,将它理想化用于 Redis 和内存中in-memory使用的案例。然而该规范仅保留了几个月在一段时间后我积累了与别人讨论的即将增加到 Redis 中的内容为了升级它使用了许多提示hint几乎从头到尾重写了一遍。我想 Redis 流尤其对于时间系列是非常有用的,而不仅是用于事件和消息类的应用程序。
当我从 Timothy 的想法萌芽,去尝试着写一个规范的时候,我使用了我用于 Redis 集群中的 radix 树实现,优化了它内部的某些部分。这为实现一个有效利用空间的日志提供了基础,而仍然可以用对数时间来访问范围。同时,我开始去读关于 Kafka 流以获得另外的灵感,它也非常适合我的设计,并且产生了一个 Kafka 消费组的概念,并且,理想化的话,它可以用于 Redis 和内存用例。然而,该规范仅维持了几个月,在一段时间后我几乎把它从头到尾重写了一遍,以便将我与别人讨论的即将增加到 Redis 中的内容所得到的许多建议一起升级。我希望 Redis 流是非常有用的,尤其对于时间序列,而不仅是用于事件和消息类的应用程序。
让我们写一些代码
=====================
### 让我们写一些代码
从 Redis 会回来后,在整个夏天,我实现了一个称为 “listpack” 的库。这个库是 ziplist.c 的继承者,那是一个表示在单个分配中字符串元素的列表的数据结构。它是一个非常专业的序列化格式,有在相反的顺序中可解析的特性,从右到左: 在所有的用户案例中用于替代 ziplists 中所需的某些东西
从 Redis 会回来后,在整个夏天,我实现了一个称为 “listpack” 的库。这个库是 ziplist.c 的继任者,那是一个表示在单个分配中的字符串元素列表的数据结构。它是一个非常特殊的序列化格式,其特点在于也能够以逆序(从右到左)解析:需要它以便在各种用例中替代 ziplists
结合 radix 树 + listpacks它可以很容易地去构建一个日志它同时也是非常高效的并且是索引化的意味着允许通过 IDs 和时间进行随机访问。自从实现这个方法后,为了实现流数据结构,我开始去写一些代码。我直到完成实现,不管怎样,在这个时候,在 Github 上的 Redis 内部的 “streams” 分支,去启动共同开发并接受订阅已经足够了。我并没有声称那个 API 是最终版本,但是,这有两个有趣的事实:一是,在那时,仅客户组是缺失的,加上一些不那么重要的命令去操作流,但是,所有的大的方面都已经实现了。二是,一旦各个方面比较稳定了之后 ,决定将所有的流的工作移植到 4.0 分支,它大约两个月后发布。这意味着 Redis 用户为了使用流,不用等待 Redis 4.2,它们将对生产使用的 ASAP 可用。这是可能的,因为有一个新的数据结构,几乎所有的代码改变都是独立于新代码的。除了阻塞列表操作之外 :代码都重构了,因此,我们和流共享了相同的代码,并且,列表阻塞操作在 Redis 内部进行了大量的简化
结合 radix 树和 listpacks它可以很容易地去构建一个日志它同时也是非常空间高效的并且是索引化的这意味着允许通过 ID 和时间进行随机访问。自从这些就绪后,我开始去写一些代码以实现流数据结构。我最终完成了该实现,不管怎样,现在,在 Github 上的 Redis 的 “streams” 分支里面,它已经可以跑起来了。我并没有声称那个 API 是 100% 的最终版本,但是,这有两个有趣的事实:一是,在那时,只有消费组是缺失的,加上一些不那么重要的命令去操作流,但是,所有的大的方面都已经实现了。二是,一旦各个方面比较稳定了之后 ,决定大概用两个月的时间将所有的流的工作向后移植到 4.0 分支。这意味着 Redis 用户为了使用流,不用等待 Redis 4.2,它们在生产环境马上就可用了。这是可能的,因为作为一个新的数据结构,几乎所有的代码改变都出现在新的代码里面。除了阻塞列表操作之外:该代码被重构了,我们对于流和列表阻塞操作共享了相同的代码,而极大地简化了 Redis 内部
教程:欢迎使用 Redis 流
==================================