Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis List实现

详解Redis中的List是如何实现的

作者:大明哥_

List 的 Redis 中的 5 种主要数据结构之一,它是一种序列集合,可以存储一个有序的字符串列表,顺序是插入的顺序,本文将给大家介绍了一下Redis中的List是如何实现的,需要的朋友可以参考下

回答

Redis 中的 List 数据结构是一个双向链表,用于存储一个序列的数据,它类似于 Java 中的数组或列表,其底层实现分为两个版本:

扩展

List 介绍

List 的 Redis 中的 5 种主要数据结构之一,它是一种序列集合,可以存储一个有序的字符串列表,顺序是插入的顺序。我们可以使用相关命令添加一个字符串元素到 List 的头部(左边)或者尾部。

**List 的最大长度为 2^31 - 1,即每个 List 支持超过 40 亿个元素。**主要特点如下:

List 常用的命令如下:

下面是 List 常用命令的演示:

需要注意的是,List 设置过期时间,只能给整个 List 设置过期时间,不能单独给某一个元素设置。

List 的底层实现

List 的底层实现有三种:zipListlinkedListquickList。他们的使用情况如下:

zipList

当 List 中的元素比较少或者每个元素的大小也小时,Redis 选择 zipList 来存储数据。zipList 通过紧凑的内存布局存储一系列的 entry,每个 entry 可以代表一个字符串或者整数。

zipList 的内部结构主要分为三个部分:表头(header)、条目(entries)和表尾(end),如下图:

entry 的结构与其存储的数据类型相关,如下:

存储字符串时,有三个部分,而整数只有两个部分,主要是因为整数是以最紧凑的格式存储,没有使用任何额外的标记或填充字节,所以在 zipList 中,整数值的编码同时包含了类型信息和实际的整数值。

记录前一个 entry 占用字节数,zipList 能实现逆序遍历就是靠这个字段确定往前移动多少字节拿到上一个 entry 首地址。

若前一个 entry 长度小于 254 个字节时,则 prevlen 占用 1 个字节。若前一个 entry 长度大于等于 254 个字节,则 prevlen 占用 5 个字节,第一个字节设为0xFE,接下来的4字节用于存储实际的长度值。

用于表示当前 entry 的类型和长度,当前 entry 的长度和值是根据保存的是整数还是字符串以及数据的长度共同来决定。

前两位用于表示类型,当前两位值为 “11” 则表示 entry 存放的是整数类型数据,其他表示存储的是字符串。

实际存储数据的位置。如果 entry 存储整数类型,则没有这个 entry-data,它会合并到 encoding 中。

zipList 适合较少元素的存储,如果元素过多的话,则查询效率会大打折扣,时间复杂度为 O(N)。除了查询效率较低外,zipList 还有一个问题:“连锁更新问题”。那什么是连锁更新问题呢?

zipList是一个紧凑的序列数据结构,它每个 entry 都有一个 prevlen 字段来记录前一个 entry 的大小,当我们插入一个较大元素或者修改一个元素较大时,会引发后续 entry 的 prevlen 占用空间发生变化,从而导致该 entry 的存储空间大小超过 254 bytes,进一步引发后续 entry 的存储空间,导致一连串的 entry 存储空间发生变化,从而引发“连锁更新”问题。

“连锁更新”通常发生在以下情况:

举一个例子:假如我们有一个 zipList ,它有多个 entry 大小在 250~253字节之间,他们的 prevlen 都是 1 个字节:

现在我们插入一个新的 entry,长度为 260 bytes,则是 e1 的 prevlen 就会由 1 bytes 增加到 5 bytes,导致 e1 存储空间由 252 bytes 增加到 256 bytes:

e1 由 252 bytes 增加到 256 bytes,那么 e2 的 prevlen 就会由 1 bytes 增加到 5 bytes,从而导致 e2 的存储空间由 252 bytes 增加到 256 bytes:

e2 的存储空间增加会导致 e3 的增加,e3 又会导致 e4,e4 又会延伸到 e5,但是 e5 的存储空间由 100bytes 增加到 104 bytes,小于 254 bytes,所以不会导致 e6 的 prevlen 值增大,至此 “连锁更新” 结束:

“连锁更新”会对 zipList 的性能有影响,因为它会导致大量的内存重新分配和数据复制。而且在极端情况下,即使只是修改了一个很小的 entry,也可能会导致整个 zipList 被复制和更新,严重影响 zipList 的性能。

总结:当列表中元素的⻓度较⼩或者数量较少时,通常采⽤zipList来存储。原因是因为 zipList 是一个紧凑的数据结构,能够有效地减少内存占用。但是,在列表中元素较多或者元素较大时,zipList 的性能会下降,因为在列表的头部或尾部添加或删除元素时,可能需要重新分配并复制整个 ziplist。所以,zipList 非常适合少量的小数据存储。同时 zipList 还有一个“连锁更新”的问题,也会严重影响 ziplist 的性能。

linkedList

linkedList 是一个由一个个节点组成的双向链表,数据结构定义如下:

typedef struct list {
    // 头指针
    listNode *head;
    // 尾指针
    listNode *tail;
    // 节点值的复制函数
    void *(*dup)(void *ptr);
    // 节点值释放函数
    void (*free)(void *ptr);
    // 节点值比对是否相等
    int (*match)(void *ptr, void *key);
    // 链表的节点数量
    unsigned long z;
} list;

list 结构体代表整个链表,包含指向链表头节点、尾节点的指针,以及链表的长度等信息。listNode 代表双向链表的一个节点,定义如下:

typedef struct listNode {
    // 前驱节点指针
    struct listNode *prev;
    // 后驱节点指针
    struct listNode *next;
    // 指向节点的值
    void *value;
} listNode;

整体结构如下:

linkedList 是一个双向链表,所以在执行两端操作时(如 LPUSHRPUSHLPOPRPOP),时间复杂度是 O(1),效率非常快。但是如果它要执行索引类操作(如 LINDEXLSET )时,则需要遍历列表,所以效率会低些。同时,linkedListzipList 相比,它的内存使用率较高,每个节点除了存储值本身外,还需要额外空间存储前向和后向的指针。但是,对于大型列表,这种额外的内存开销是合理的,因为它提供了更佳的操作性能。所以 linkedList 比较适合列表元素数量较多,或者列表中包含大型元素的场景。

quicklist

quicklist 出现之前,List 使用 zipListlinkedList 来存储数据。

zipListlinkedList都存在这样或那样的缺陷,所以 Redis 在 3.2 版本采用 quicklist 来取代zipListlinkedList

quicklist 是将 zipList 作为节点嵌入 linkedList 的节点中,它结合了两者的优点。具体来说,quicklist 是由多个 ziplist 节点组成的 linkedList 链表,每个 ziplist 节点可以存储多个列表元素。

quicklist 表头结构:

typedef struct quicklist {
    // 链表头部节点指针
    quicklistNode *head;
    
    // 链表头部节点指针
    quicklistNode *tail;
    
    // 所有 ziplist 的总 entry 个数
    unsigned long count;
    
    // quicklistNode 个数
    unsigned long len;          /* number of quicklistNodes */
    signed int fill : QL_FILL_BITS;       /* fill factor for individual nodes */
    unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;

quicklist节点结构:

typedef struct quicklistNode {
    // 前驱节点指针
    struct quicklistNode *prev;
    // 后继节点指针
    struct quicklistNode *next;
    // 指向 ziplist 的指针
    unsigned char *zl;
    // ziplist 字节大小
    unsigned int sz;
    // ziplst 元素个数
    unsigned int count : 16;
    // 编码格式,1 = RAW 代表未压缩原生ziplist,2=LZF 压缩存储
    unsigned int encoding : 2;
    // 节点持有的数据类型,默认值 = 2 表示是 ziplist
    unsigned int container : 2;
    // 节点持有的 ziplist 是否经过解压, 1 表示已经解压过,下一次操作需要重新压缩。
    unsigned int recompress : 1;
    // ziplist 数据是否可压缩,太小数据不需要压缩
    unsigned int attempted_compress : 1;
    // 预留字段
    unsigned int extra : 10;
} quicklistNode;

结合 quicklist 和 quicklistNodequicklist 的结构如下图:

使用 quicklist 关键点就在于我们如何平衡好每个 ziplist 的大小或者元素个数,平衡内存得使用和操作性能。

所以,我们需要通过配置来平衡每个 ziplist 的大小或者元素个数。Redis 提供了参数 list-max-ziplist-sizelist-compress-depth 来配置 quicklist

list-max-ziplist-size

控制每个 quicklist 节点内部的 ziplist 可以包含的最大元素数量或字节大小。

当一个 ziplist 中的元素达到配置的阈值时,如果有新元素添加到列表中时,Redis 会创建一个新的 ziplist 节点来存储这个元素。该值较大(绝对值)时,单个 ziplist 存储的元素就越多,内存利用率就越高,但是会牺牲列表的操作性能,如果较小,则有利于列表的操作性能,但牺牲了内存的利用率。所以,在实际生产情况下我们需要根据实际情况配置一个适中的值,来平衡列表操作的内存效率和性能。

Redis 默认 list-max-ziplist-size-2,限制 ziplist 节点大小为 2KB。

list-compress-depth

用于配置 quicklist 中节点的压缩。list-compress-depth 决定了在 quicklist 中,距离首尾元素多远的中间节点应该被压缩存储,该参数影响着内存使用和访问这些被压缩节点数据时的性能。注意,为了 push/pop 操作的高效性,quicklist 的头和尾节点永远都不会被压缩。

以上就是详解Redis中的List是如何实现的的详细内容,更多关于Redis List实现的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文