ziplist 是 Redis(一个内存数据存储系统)中使用的一种特殊数据结构。它旨在有效地存储一系列小尺寸元素,例如字符串、整数或浮点数。
ziplist 是一种压缩列表表示形式,可优化内存使用并提供对元素的有效访问。它通过使用顺序布局来实现这一点,其中元素以紧凑格式一个接一个地存储。这消除了单独进行内存分配的需要,并减少了内存开销。
ziplist 的结构由一系列条目组成。每个条目代表一个元素,并包含以下组件:
ziplist 中的元素是连续存储的,这意味着它们之间没有指针或额外的元数据。与链表等其他数据结构相比,这种紧凑表示减少了内存开销,因为链表需要额外的内存来存储指针和元数据。
ziplist 还通过允许直接索引来提供对元素的有效访问。由于元素是顺序存储的,因此可以在常量时间内访问特定索引处的元素。这在按位置检索元素或执行基于范围的操作时特别有用。
Redis 会根据特定条件自动在 ziplist 和其他数据结构(例如链表或哈希表)之间切换。使用 ziplist 的决定取决于元素的数量及其大小等因素。Redis 提供了配置选项来控制不同表示形式之间切换的阈值。
为了理解 ziplist 的潜在效率,我们来检查一下名为 LIST 的基本结构。在典型的双向链表中,节点代表列表中的每个值。这些节点包含指向前一个和后一个节点的指针,以及指向节点内字符串的指针。每个字符串值都存储为三个部分:一个表示长度的整数,一个表示剩余空闲字节数的整数,以及字符串本身后跟一个空字符。图 9.1 提供了这种结构的一个示例,展示了字符串值“one”、“two”和“ten”作为更大链表的一部分。
然而,如果我们忽略一些使得链表看起来不太有利的额外细节,我们就会意识到,这三个字符串(每个由三个字符组成)都需要大量的开销。实际上,它们占用了三个指针、两个整数(长度和剩余字节)、字符串以及一个额外字节的空间。在 32 位平台上,存储仅仅 3 字节的实际数据需要 21 字节的开销。请注意,这个估算低估了实际的存储需求。
另一方面,ziplist 通过存储一系列长度、长度和字符串元素来提供更高效的表示形式。第一个长度代表前一个条目的大小,便于在两个方向上扫描;第二个长度表示当前条目的大小;字符串表示存储的数据本身。尽管这些长度的实际影响还有更多细节,但对于前面提到的三个示例字符串,长度仅需要每个 1 字节。因此,在此示例中,ziplist 设法将每个字符串的开销从 21 字节减少到大约 2 字节。
现在,我们来探讨如何确保紧凑的 ziplist 编码得到利用。
利用 ZIPLIST 编码
为了确保选择性地使用 ziplist 表示形式并最大限度地减少内存消耗,Redis 包含了六个配置选项。这些选项(如列表 9.1 所示)决定了何时将 ziplist 表示形式应用于 LIST、HASH 和 ZSET。
不同结构的 ziplist 表示配置选项
LIST、HASH 和 ZSET 的基本配置选项结构相似,包括“-max-ziplist-entries”设置和“-max-ziplist-value”设置。它们在这三种情况下的语义基本相同。“entries”设置指定了每种数据结构允许使用 ziplist 编码的最大项目数。另一方面,“value”设置表示每个单独条目的最大大小(以字节为单位)。如果超出其中任何一个限制,Redis 会将该结构(LIST、HASH 或 ZSET)转换为非 ziplist 表示形式,从而增加内存使用量。
默认情况下,Redis 2.6 的安装应该与列表 9.1 中提供的设置相同。让我们通过添加项目并检查其表示形式来实验一个简单 LIST 对象的 ziplist 表示形式,如下所示。
确定结构的 ziplist 存储
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 使用 ziplist 编码存储,占用 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 仍然使用 ziplist 表示形式,其大小已增加到 36 字节。此增加量恰好对应于新添加的四个项目中每个项目 2 字节的开销(1 字节用于数据)。
‘type’: ‘Value’}
conn.rpush(‘test’, 65*’a’)
9
conn.debug_object(‘test’)
{‘encoding’: ‘linkedlist’, ‘refcount’: 1, ‘lru_seconds_idle’: 10,
当推入的项目大于允许的编码限制时,LIST 会自动从 ziplist 编码转换为标准的链表表示形式。
‘lru’: 274851, ‘at’: ‘0xb6c9f120’, ‘serializedlength’: 30,
尽管序列化长度减小了,但需要注意的是,对于非 ziplist 编码(SET 的特殊编码除外),此数字不能准确反映实际的内存消耗。
‘type’: ‘Value’}
conn.rpop(‘test’)
‘aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa’
conn.debug_object(‘test’)
{‘encoding’: ‘linkedlist’, ‘refcount’: 1, ‘lru_seconds_idle’: 0,
一旦 ziplist 转换为常规结构,即使之后满足 ziplist 编码的条件,它仍将保持该形式。
‘lru’: 274853, ‘at’: ‘0xb6c9f120’, ‘serializedlength’: 17,
‘type’: ‘Value’}
随着新的 DEBUG OBJECT 命令的引入,确定对象是否使用 ziplist 存储成为一种减少内存消耗的有用方法。
值得注意的是,有一种结构明显不使用特殊的 ziplist 编码,那就是 SET。尽管 SET 也具有紧凑的表示形式,但它们具有不同的语义和限制,我们将在接下来的章节中探讨。