Redis的底层数据结构是什么?


305

我试图在一个明确的列表中回答两个问题:

  1. Redis的底层数据结构是什么?
  2. 每种类型的主要优点/缺点/用例是什么?

因此,我读过Redis列表实际上是用链接列表实现的。但是对于其他类型,我无法提取任何信息。同样,如果有人偶然发现了这个问题,而又对修改或访问不同数据结构的优缺点没有一个高层次的总结,那么他们将有完整的清单,列出何时可以最佳地使用特定类型进行引用。

具体来说,我希望概述所有类型:字符串,列表,集合,zset和哈希。

到目前为止,我已经看过这些文章,其中包括:


7
如何使用服务器琐事?如何确定何时使用一种编程结构而不是另一种编程结构?这直接适用于编程,因为我会针对不同的用途使用不同的类型。
Homer6

2
如何使用服务器不一定是琐碎的事,但它是不合时宜的-并不是您所要求的。用于特定目的的数据结构将是主题,但这也不是您要的。Redis碰巧使用的琐事,没有其他理由说明为什么他们在特定情况下使用特定结构-在这一点上,我们回到我刚才所说的主题,而Redis恰好要做的是不相关的。
杰里·科芬

5
该主题明确指出:“数据结构是什么,何时应使用不同的类型?” 那个话题如何?您是说学习链接列表,哈希和数组与编程无关吗?因为,我认为它们是直接相关的-尤其是在主要针对性能而设计的服务器中。同样,它们也很重要,因为错误的选择可能意味着从一个应用程序到下一个应用程序的性能大大降低。
Homer6

19
antirez的回答赎回了这个问题。不利于程序员,并分散了各地的用户。
约翰·希恩

75
@JerryCoffin受到大家的尊敬,redis是一种软件开发工具,询问有关软件开发工具的问题已成为主题。“您可以从源头获得答案”这一事实并不是一个直接的原因……要从源头上获得答案将需要几个小时。Redis的使用非常广泛,因此这个问题不太局限。Stack Overflow的全部目的在于学习编程,并询问一种广为流行的编程工具所使用的数据结构是否有助于实现该目标。简而言之,我找不到解决此问题的任何理由。
乔尔·斯波斯基

Answers:


612

我将尝试回答您的问题,但首先我会从一开始可能看起来很奇怪的事情开始:如果您对Redis内部不感兴趣,则不必在意内部如何实现数据类型。这是有一个简单的原因:对于每个Redis操作,您都会在文档中找到时间复杂度,并且,如果您拥有一组操作和时间复杂度,则唯一需要做的就是了解内存使用情况的一些线索(并且因为我们会根据数据进行很多优化,获取这些数据的最佳方法是进行一些琐碎的实际测试。

但是,正如您所问的那样,这是每种Redis数据类型的基础实现。

  • 字符串是使用C动态字符串库实现的,因此我们无需为附加操作中的分配支付费用(渐近而言)。这样,例如,我们有O(N)个附加项,而不是具有二次行为。
  • 列表通过链接列表实现。
  • 散列与哈希表来实现。
  • 排序集通过跳过列表(平衡树的一种特殊类型)实现。

但是,如果列表,集合和排序集合的项目数少且最大值大,则使用不同的,更紧凑的编码。对于不同的类型,此编码有所不同,但其特点是它是一个紧凑的数据块,通常会为每个操作强制进行O(N)扫描。由于我们仅将这种格式用于小物体,因此这不是问题。扫描一个很小的O(N)Blob可以忽略高速缓存,因此从实践上讲它是非常快的,并且当元素过多时,编码会自动切换到本机编码(链接列表,哈希等)。

但是您的问题不仅仅在于内部,而是您要使用哪种类型来完成任务?

弦乐

这是所有类型的基本类型。它是四种类型之一,也是复杂类型的基本类型,因为List是字符串列表,Set是一组字符串,依此类推。

在要存储HTML页面的所有显而易见的情况下,以及在避免转换已编码数据的情况下,Redis字符串都是一个好主意。因此,例如,如果您具有JSON或MessagePack,则可以将对象存储为字符串。在Redis 2.6中,您甚至可以使用Lua脚本来操纵这种对象服务器端。

字符串的另一种有趣用法是位图,通常是字节的随机访问数组,因为Redis导出命令以访问字节的随机范围甚至单个位。例如,查看以下优秀博客文章:使用Redis的Fast Easy实时指标

清单

当您可能只触摸列表的极端时(靠近尾巴或靠近头部),列表是不错的选择。列表不是很好的分页内容,因为随机访问速度很慢,O(N)。因此,列表的良好用法是普通队列和堆栈,或者使用具有相同源和目标的RPOPLPUSH在循环中处理项目以“旋转”项目环。

当我们只想创建N个项目的封顶集合时,通常我们只访问顶部或底部项目,或者当N小时,列表也很好。

套装

集合是无序的数据集合,因此每次您拥有一个项目集合时它们都很好,并且以非常快速的方式检查集合的存在或大小非常重要。关于集的另一件很酷的事情是支持偷看或弹出随机元素(SRANDMEMBER和SPOP命令)。

集合也可以很好地表示关系,例如“用户X的朋友是什么?” 等等。但是,正如我们将看到的,用于这种东西的其他好的数据结构是排序集。

集合支持复杂的操作,例如交集,并集等,因此当您有数据并且想要对该数据执行转换以获得一些输出时,这是一种以“计算”方式使用Redis的良好数据结构。

小集合以非常有效的方式编码。

散列

散列是代表由字段和值组成的对象的理想数据结构。散列字段也可以使用HINCRBY自动增加。当您有对象(例如用户,博客文章或其他类型的项目)时,如果您不想使用自己的编码(例如JSON或类似格式),则很可能要使用哈希。

但是,请记住,Redis非常有效地编码了小哈希,并且您可以要求Redis以非常快速的方式原子地获取,设置或递增各个字段。

哈希还可以用于使用引用来表示链接的数据结构。例如,查看lamernews.com评论的实现。

排序集

除列表外,排序集是唯一其他可维护有序元素的数据结构。您可以使用排序集来做很多很酷的事情。例如,您可以在Web应用程序中拥有各种Top Something列表。按分数排名最高的用户,按浏览量排名的最高帖子,排名最高的东西,但是单个Redis实例将支持每秒大量的插入和get-top-elements操作。

像常规集一样,已排序的集可用于描述关系,但是它们也允许您分页项目列表并记住排序。例如,如果我记得带有排序集的用户X的朋友,我可以轻松地按照接受的朋友的顺序记住他们。

排序集适合于优先级队列。

排序集就像更强大的列表一样,从列表中间插入,删除或获取范围总是非常快。但是它们使用更多的内存,并且是O(log(N))数据结构。

结论

我希望我在这篇文章中提供了一些信息,但是最好从http://github.com/antirez/lamernews下载lamernews的源代码并了解其工作原理。Lamer News内部使用了Redis的许多数据结构,并且有很多线索可以用来解决给定的任务。

抱歉语法错误,在这里是午夜,又太累了,无法阅读这篇文章;)


45
这是Redis的唯一作者。我给他发了电子邮件,请他回复。非常非常感谢Salvatore。这是很好的信息。
Homer6

58
谢谢,但是我不是唯一的大贡献者,Pieter Noordhuis提供了当前实现的很大一部分:)
antirez 2012年

1
如果相同的字符串位于许多不同的集合中,那么将仅存储该字符串的单个副本吗?
sbrian'5

仅使用跳过列表,zscore在O(1)中的状态如何?
2013年

1
尽管跳过列表不是适当的平衡树,但是您可以将跳过列表视为“反向”随机树。即使实现和布局不同,它们基本上是等效的。
antirez 2015年

80

大多数时候,您不需要了解Redis使用的基础数据结构。但是,有些知识可以帮助您权衡CPU v / s内存。它还可以帮助您以有效的方式对数据建模。

在内部,Redis使用以下数据结构:

  1. 字典
  2. 双链表
  3. 跳过清单
  4. 邮递区号清单
  5. 整数集
  6. Zip Maps(自Redis 2.6开始不赞成使用zip列表)

要查找特定键使用的编码,请使用命令object encoding <key>

1.琴弦

在Redis中,字符串称为简单动态字符串或SDS。这是一个较小的包装器char *,可让您存储字符串的长度和可用字节数作为前缀。

因为存储了字符串的长度,所以strlen是O(1)运算。另外,由于长度是已知的,因此Redis字符串是二进制安全的。字符串包含空字符是完全合法的。

字符串是Redis中最通用的数据结构。字符串是以下所有内容:

  1. 可以存储文本的字符串。请参阅SETGET命令。
  2. 可以存储二进制数据的字节数组。
  3. 一个long可以存储数字。请参阅INCRDECRINCRBYDECRBY命令。
  4. 的阵列(的charsintslongs其能够允许高效的随机存取或任何其他数据类型)。请参阅SETRANGEGETRANGE命令。
  5. 一个位阵列,允许您设置或获取各个位。请参阅SETBITGETBIT命令。
  6. 您可以用来构建其他数据结构的一块内存。它在内部用于构建ziplist和intset,它们是紧凑的,内存有效的数据结构,适用于少量元素。在下面的更多内容。

2.字典

Redis使用以下字典

  1. 要将键映射到其关联值,其中value可以是字符串,哈希,集合,排序集合或列表。
  2. 将密钥映射到其到期时间戳。
  3. 要实现哈希,集合和排序集合数据类型。
  4. 将Redis命令映射到处理这些命令的功能。
  5. 要将Redis密钥映射到该密钥上阻止的客户端列表。参见BLPOP

Redis词典使用哈希表实现。除了说明实现之外,我仅介绍Redis的具体内容:

  1. 字典使用一种称为的结构dictType来扩展哈希表的行为。此结构具有函数指针,因此以下操作是可扩展的:a)哈希函数,b)键比较,c)键析构函数,和d)值析构函数。
  2. 字典使用murmurhash2。(以前,他们使用djb2哈希函数,seed = 5381,但随后将哈希函数切换为murmur2。有关djb2哈希算法的说明,请参见此问题。)
  3. Redis使用增量哈希,也称为增量调整大小。该词典有两个哈希表。每次触摸字典时,都会将一个存储桶从第一个(较小的)哈希表迁移到第二个。这样,Redis可以防止执行昂贵的调整大小操作。

Set数据结构使用字典,以保证没有重复。在Sorted Set使用字典的元素映射到它的分数,这是为什么ZSCORE是O(1)的操作。

3.双链表

list数据类型是使用实现的双向链表。Redis的实现是直接从算法教科书开始的。唯一的变化是Redis将长度存储在列表数据结构中。这样可以确保LLEN具有O(1)复杂度。

4.跳过列表

Redis使用“ 跳过列表”作为“排序集”的基础数据结构。维基百科有很好的介绍。威廉·普格(William Pugh)的论文“ 跳过列表:平衡树的概率替代方法”有更多详细信息。

排序集同时使用“跳过列表”和“字典”。字典存储每个元素的分数。

Redis的“跳过列表”实现在以下方面与标准实现不同:

  1. Redis允许重复分数。如果两个节点的分数相同,则按词典顺序对其进行排序。
  2. 每个节点在级别0处都有一个后向指针。这使您可以按得分的相反顺序遍历元素。

5.邮递区号清单

压缩列表类似于双向链接列表,不同之处在于它不使用指针并且以内联方式存储数据。

双向链表中的每个节点都有3个指针-一个前向指针,一个后向指针和一个指针来引用存储在该节点上的数据。指针需要内存(在64位系统上为8字节),因此对于小列表,双向链接列表效率很低。

压缩列表按顺序在Redis字符串中存储元素。每个元素都有一个小标题,用于存储元素的长度和数据类型,下一个元素的偏移量以及上一个元素的偏移量。这些偏移量替换了前进和后退指针。由于数据是内联存储的,因此我们不需要数据指针。

Zip列表用于存储小列表,排序集和哈希。排序后的数据集被平整为一个列表,[element1, score1, element2, score2, element3, score3]并存储在“压缩列表”中。散列成扁平状的清单[key1, value1, key2, value2]等。

使用压缩列表,您可以在CPU和内存之间进行权衡。Zip列表可以节省内存,但是它们比链接列表(或哈希表/ Skip列表)使用更多的CPU。在zip列表中找到一个元素是O(n)。插入新元素需要重新分配内存。因此,Redis仅将此编码用于小型列表,哈希和排序集。您可以通过更改redis.conf 中<datatype>-max-ziplist-entriesand 的值来调整此行为<datatype>-max-ziplist-value>。有关更多信息请参见Redis内存优化,“小聚合数据类型的特殊编码”部分

上ziplist.c意见都非常出色,并且可以完全理解这种数据结构,而不必阅读代码。

6.整数集

整数集是“排序整数数组”的奇特名称。

在Redis中,通常使用哈希表来实现集合。对于小集合,哈希表在内存方面是低效的。当集合仅由整数组成时,数组通常更有效。

整数集是整数的排序数组。为了找到元素,使用了二进制搜索算法。这具有O(log N)的复杂度。向此数组添加新的整数可能需要重新分配内存,这对于大型整数数组可能会变得昂贵。

作为进一步的内存优化,整数集有3种变体,具有不同的整数大小:16位,32位和64位。Redis足够聪明,可以根据元素的大小使用正确的变体。添加新元素且其大小超过当前大小时,Redis会自动将其迁移到下一个大小。如果添加了字符串,Redis会自动将Int集转换为基于常规哈希表的集。

整数集是CPU和内存之间的权衡。整数集具有极高的内存效率,对于小型集合,它们比哈希表要快。但是在经过一定数量的元素后,O(log N)的检索时间和重新分配内存的成本变得太大。根据实验,发现切换到常规哈希表的最佳阈值为512。但是,您可以根据应用程序的需要增加此阈值(减小此阈值没有意义)。参见set-max-intset-entriesredis.conf。

7.邮编

邮编地图是扁平化的字典,并存储在列表中。它们与邮编列表非常相似。

自Redis 2.6起已弃用Zip Maps,并且将小哈希存储在Zip List中。要了解有关此编码的更多信息,请参阅zipmap.c中注释


2

Redis存储指向值的键。键可以是任意大小不超过合理大小的二进制值(建议使用短ASCII字符串以提高可读性和调试性)。值是五种本地Redis数据类型之一。

1.strings —二进制安全字节序列,最大为512 MB

2.哈希-键值对的集合

3.lists —插入顺序的字符串集合

4.sets —唯一字符串的集合,无顺序

5.sorted sets —一组按用户定义的评分排序的唯一字符串

弦乐

Redis字符串是字节序列。

Redis中的字符串是二进制安全的(这意味着它们的已知长度不受任何特殊的终止字符确定),因此,您可以在一个字符串中存储高达512 MB的任何内容。

字符串是规范的“键值存储”概念。您有一个指向值的键,其中键和值都是文本或二进制字符串。

有关字符串的所有可能操作,请参见 http://redis.io/commands/#string

散列

Redis哈希是键值对的集合。

Redis哈希表包含许多键值对,其中每个键和值都是一个字符串。Redis哈希不直接支持复杂值(意味着,您不能让哈希字段具有列表或集合或另一个哈希值),但是可以使用哈希字段指向其他顶级复杂值。您可以对哈希字段值执行的唯一特殊操作是数字内容的原子递增/递减。

您可以通过两种方式想到Redis哈希:作为直接的对象表示形式以及作为紧凑存储许多小值的方式。

直接对象表示很容易理解。对象具有名称(哈希键)和具有值的内部键的集合。参见下面的示例。

使用哈希存储许多小的值是聪明的Redis大规模数据存储技术。当散列具有少量字段(约100个)时,Redis会优化整个散列的存储和访问效率。Redis的小型哈希存储优化引发了一个有趣的行为:拥有100个带有100个内部键和值的散列比拥有10,000个指向字符串值的顶级键更有效。使用Redis哈希以这种方式优化数据存储确实需要额外的编程开销来跟踪数据的最终存储位置,但是,如果您的数据存储主要是基于字符串的,则可以使用这一怪异的技巧节省大量的内存开销。

有关散列的所有可能操作,请参见哈希文档

清单

Redis列表的行为类似于链接列表。

您可以从列表的开头或结尾插入列表,从列表删除和遍历列表。

当您需要按插入顺序维护值时,请使用列表。(Redis确实为您提供了插入任意列表位置的选项,但是如果您插入的位置离起始位置较远,插入性能将会降低。)

Redis列表通常用作生产者/消费者队列。将项目插入列表,然后从列表中弹出项目。如果您的消费者尝试从没有元素的列表中弹出,会发生什么情况?您可以要求Redis等待某个元素出现,并在添加元素后立即将其返回给您。这将Redis变成实时消息队列/事件/作业/任务/通知系统。

您可以从列表的任一端原子删除元素,从而将任何列表视为堆栈或队列。

您还可以通过在每次插入后将列表修整为特定大小来维护定长列表(加盖的集合)。

有关列表上所有可能的操作,请参阅列表文档

套装

Redis集是集合。

Redis集包含唯一的无序Redis字符串,其中每个字符串每个集仅存在一次。如果将相同的元素添加到集合十次,则只会显示一次。集合非常适合懒惰地确保某物至少存在一次,而不必担心重复元素的积累和浪费。您可以根据需要多次添加相同的字符串,而无需检查它是否已经存在。

集合可以快速进行成员资格检查,插入和删除集合中的成员。

正如您所期望的,集合具有高效的集合操作。您可以一次获取多个集合的并集,相交和差。结果可以返回给调用者,也可以将结果存储在新集中以供以后使用。

集合具有对成员资格检查(与列表不同)的恒定时间访问权限,并且Redis甚至具有方便的随机成员删除和返回(“从集合中弹出一个随机元素”)或随机成员返回而无需替换(“给我30个具有随机性的唯一用户) ”或更换(“给我7张卡,但每次选择后,将卡放回去,以便有可能再次采样”)。

有关集合的所有可能操作,请参阅集合文档

排序集

Redis排序集是具有用户定义顺序的集。

为简单起见,您可以将排序后的集合视为具有唯一元素的二叉树。(Redis排序的集合实际上是跳过列表。)元素的排序顺序由每个元素的得分定义。

排序的集合仍然是集合。元素只能在集合中出现一次。出于唯一性目的,元素由其字符串内容定义。插入排序分数为3的元素“苹果”,然后插入排序分数为500的元素“苹果”会在您的排序集中产生一个排序分数为500的元素“苹果”。集仅基于数据是唯一的,而不基于(分数,数据)对。

确保您的数据模型依赖于字符串内容,而不是元素的唯一性分数。分数可以重复(甚至为零),但是最后一次,每个排序的set元素只能存在一次。例如,如果您尝试通过将得分的分数作为登录纪元和用户ID的值作为排序集来存储每个用户登录的历史记录,那么最终将只为所有用户存储最后一个登录纪元。您的设置将增长到用户群的大小,而不是您所需的用户群*登录名的大小。

元素会随分数添加到您的集合中。您可以随时更新任何元素的分数,只需使用新分数再次添加该元素。分数由浮点双精度表示,因此您可以根据需要指定高精度时间戳的粒度。多个元素可能具有相同的分数。

您可以通过几种不同的方式检索元素。由于所有内容均已排序,因此您可以要求分数最低的元素。您可以要求分数最高的元素(“反向”)。您可以按其排序分数以自然或逆序要求元素。

有关排序集的所有可能操作,请参阅排序集文档。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.