dot 快速的未来即将在您所在的城市举行活动。

加入我们参加 Redis 发布会

Redis 压缩列表

返回术语表

什么是压缩列表?

压缩列表是 Redis(一个内存数据存储系统)中使用的一种专用数据结构。它旨在有效地存储一系列小尺寸元素,例如字符串、整数或浮点数。

压缩列表是一种压缩的列表表示形式,它优化了内存使用并提供了对元素的有效访问。它通过使用连续布局来实现这一点,其中元素以紧凑的格式依次存储。这消除了对单独内存分配的需求,并减少了内存开销。

压缩列表的结构由一系列条目组成。每个条目表示一个元素,并包含以下组件

  1. Prevlen:此字段存储前一个条目的长度。它允许在压缩列表中有效地向前和向后遍历。
  2. Entrylen:此字段存储当前条目的长度。
  3. Content:此字段包含元素的实际数据,例如字符串、整数或浮点值。

压缩列表中的元素按顺序存储,这意味着它们之间没有指针或其他元数据。与其他数据结构(例如链表)相比,这种紧凑的表示减少了内存开销,链表需要额外的内存来存储指针和元数据。

压缩列表还通过允许直接索引来提供对元素的有效访问。由于元素按顺序存储,因此可以在恒定时间内访问特定索引处的元素。这在通过其位置检索元素或执行基于范围的操作时特别有用。

Redis 根据某些条件在压缩列表和其他数据结构(例如链表或哈希表)之间自动切换。使用压缩列表的决定取决于诸如元素数量和大小等因素。Redis 提供配置选项来控制在不同表示之间切换的阈值。

Redis 压缩列表最佳实践

为了了解压缩列表的潜在效率,让我们检查一下称为 LIST 的基本结构。在典型的双向链表中,节点表示列表中的每个值。这些节点包含指向前一个节点和下一个节点的指针,以及指向节点内字符串的指针。每个字符串值存储为三个部分:一个表示长度的整数、一个表示剩余空闲字节数的整数以及字符串本身,后跟一个空字符。图 9.1 提供了此结构的一个示例,展示了“one”、“two”和“ten”字符串值作为较大链表的一部分。

但是,如果我们忽略一些使链表显得不太有利的额外细节,我们会意识到,这三个字符串(每个包含三个字符)都需要大量的开销。事实上,它们占用了三个指针、两个整数(长度和剩余字节)、字符串以及一个额外字节的空间。在 32 位平台上,这相当于 21 字节的开销才能存储仅仅 3 字节的实际数据。请注意,这种估计低估了实际存储需求。

另一方面,压缩列表通过存储一系列长度、长度和字符串元素来提供更有效的表示。第一个长度表示前一个条目的大小,便于双向扫描,第二个长度表示当前条目的大小,字符串表示存储的数据本身。尽管关于这些长度的实际含义还有进一步的细节,但对于前面提到的三个示例字符串,长度只需要 1 个字节。因此,在本例中,压缩列表设法将每个字符串的开销从 21 字节减少到大约 2 字节。

现在,让我们探讨如何确保使用紧凑的压缩列表编码。

使用压缩列表编码
为了保证选择性地使用压缩列表表示并最大限度地减少内存消耗,Redis 集成了六个配置选项。这些选项如清单 9.1 所示,决定了何时将压缩列表表示应用于 LIST、HASH 和 ZSET。

不同结构压缩列表表示的配置选项

LIST、HASH 和 ZSET 的基本配置选项共享类似的结构,包括“-max-ziplist-entries”设置和“-max-ziplist-value”设置。它们在所有三个情况下的语义基本相同。“entries”设置指定了相应数据结构中允许使用的最大条目数,以便采用压缩列表编码。另一方面,“value”设置指示每个单独条目的最大大小(以字节为单位)。如果这两个限制中的任何一个被超过,Redis 将结构(LIST、HASH 或 ZSET)转换为非压缩列表表示,从而增加内存使用量。

默认情况下,Redis 2.6 安装应该与清单 9.1 中提供的设置相同。让我们通过添加项目并检查其表示来试验简单 LIST 对象的压缩列表表示,如以下清单所示。

确定结构的压缩列表存储

conn.rpush(‘test’, ‘a’, ‘b’, ‘c’, ‘d’)
4
我们首先将四个项目推送到 LIST 上。

conn.debug_object(‘test’)
为了获取有关特定对象的的信息,我们可以使用“debug object”命令。

{‘encoding’: ‘ziplist’, ‘refcount’: 1, ‘lru_seconds_idle’: 20,
‘lru’: 274841, ‘at’: ‘0xb6c9f120’, ‘serializedlength’: 24,
‘type’: ‘Value’}
我们寻找的关键信息是“encoding”字段,它告诉我们此 LIST 使用压缩列表编码存储,并占用 24 字节的内存。

conn.rpush(‘test’, ‘e’, ‘f’, ‘g’, ‘h’)
8
接下来,我们将四个项目追加到 LIST 上。

conn.debug_object(‘test’)

{‘encoding’: ‘ziplist’, ‘refcount’: 1, ‘lru_seconds_idle’: 0,
‘lru’: 274846, ‘at’: ‘0xb6c9f120’, ‘serializedlength’: 36,
LIST 保持在压缩列表表示中,其大小已增加到 36 字节。此增加恰好对应于四个新添加项目中每个项目 2 字节的开销(1 字节用于数据)。

‘type’: ‘Value’}

conn.rpush(‘test’, 65*’a’)
9
conn.debug_object(‘test’)
{‘encoding’: ‘linkedlist’, ‘refcount’: 1, ‘lru_seconds_idle’: 10,
当推入一个大于允许编码限制的项目时,LIST 将自动从压缩列表编码转换为标准链表表示。

‘lru’: 274851, ‘at’: ‘0xb6c9f120’, ‘serializedlength’: 30,
虽然序列化长度减少了,但需要注意的是,对于非压缩列表编码(除了 SET 的特殊编码外),此数字并不准确地反映实际内存消耗。

‘type’: ‘Value’}

conn.rpop(‘test’)
‘aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa’
conn.debug_object(‘test’)
{‘encoding’: ‘linkedlist’, ‘refcount’: 1, ‘lru_seconds_idle’: 0,
一旦压缩列表转换为常规结构,即使它后来满足了压缩列表编码的条件,它也将保持这种形式。

‘lru’: 274853, ‘at’: ‘0xb6c9f120’, ‘serializedlength’: 17,
‘type’: ‘Value’}

随着新的 DEBUG OBJECT 命令的引入,确定对象是否使用压缩列表存储成为一种减少内存消耗的有用方法。

值得注意的是,特殊压缩列表编码中明显缺失的一种结构是 SET。虽然 SET 也具有紧凑的表示形式,但它们具有不同的语义和限制,我们将在接下来的部分中探讨。