首页 > 数据库 >Redis基础数据结构之 quicklist 和 listpack 源码解读

Redis基础数据结构之 quicklist 和 listpack 源码解读

时间:2024-09-19 10:23:48浏览次数:3  
标签:ENCODING quicklistNode Redis ziplist quicklist 源码 entry listpack

目录标题

quicklist

为什么要设计 quicklist?

ziplist 有两个问题

  1. 不能保存过多的元素,否则访问性能会下降
  2. 不能保存过大的元素,否则容易导致内存重新分配,甚至引起连锁更新

quicklist特点

quicklist 的设计,其实是结合了链表和 ziplist 各自的优势。简单来说,一个 quicklist 就是一个链表,而链表中的每个元素又是一个 ziplist。

  • 结构定义:quicklist.h
  • 实现:quicklist.c

ziplist

Ziplist(压缩列表)之所以在特定场景下效率高,主要是因为它在内存使用方面进行了优化,特别是在存储小数据集时能够显著减少内存占用。

内存效率
Ziplist 通过对存储的数据进行紧凑编码来减少内存使用。它不仅记录了数据的实际内容,还记录了每个元素的长度信息,使得读取时可以快速确定每个元素的边界。这样做的好处是,对于短字符串或小整数,Ziplist 可以以非常紧凑的方式存储,从而节省内存。

快速访问
Ziplist 中每个元素的长度信息是显式存储的,这意味着访问特定元素时,可以直接跳转到相应的位置,无需遍历整个列表。这样,即使列表中有许多元素,访问特定元素的效率仍然很高。

Ziplist中的显式长度信息确实有助于定位元素,但它并不提供真正的随机访问能力。对于连续访问或数据量较小的情况,Ziplist的表现是不错的,但在需要频繁随机访问的场景下,可能需要考虑其他数据结构,如哈希表或索引结构。

假设Ziplist中存储了三个元素:“foo”(长度3)、“bar”(长度3)和"baz"(长度3)。为了找到第三个元素"baz",你需要先读取第一个元素的长度(3字节),然后加上第二个元素的长度(再加3字节),才能定位到第三个元素的位置。

适合小数据集
Ziplist 主要适用于存储少量且大小适中的元素。当列表或哈希中的元素数量不多,且每个元素的大小也不大时,使用Ziplist可以大大节省内存。例如,对于包含少量字符串或整数的列表,Ziplist的紧凑存储方式可以减少内存开销。

紧凑存储
Ziplist使用紧凑的数据格式存储整数和字符串,通过使用不同的编码方式来适应不同类型的数据。例如,对于整数,Ziplist会根据整数值的大小选择合适的字节数(如1字节、2字节、4字节或8字节)来存储,这样可以有效地利用内存。

适用场景
Ziplist特别适用于内存敏感的应用场景,例如在存储大量小字符串或小整数时。当数据量不大时,Ziplist可以提供较高的性能和较低的内存使用。

自动转换
当Ziplist中的元素数量或大小超过了预先设定的阈值时,Redis会自动将其转换为其他数据结构,如linkedlist或hashtable,以保持良好的性能。这种机制确保了在数据量增长时,Redis仍能保持高效。

Ziplist通过紧凑的数据编码和显式的长度信息记录,能够在存储小数据集时提供高效的内存使用和快速的数据访问。然而,对于大型数据集,Ziplist的性能优势会减弱,此时Redis会自动选择更合适的数据结构来替代。

quicklist数据结构

quicklist 是一个链表,所以每个 quicklistNode 中,都包含了分别指向它前序和后序节点的指针* prev 和* next。同时,每个 quicklistNode 又是一个 ziplist,所以,在 quicklistNode 的结构体中,还有指向 ziplist 的指针* zl。

每个元素节点 quicklistNode 的定义如下:

typedef struct quicklistNode {
    // 前一个 quicklistNode
    struct quicklistNode *prev;
    // 后一个 quicklistNode
    struct quicklistNode *next;
    // quicklistNode 指向的 ziplist
    unsigned char *zl;
    // ziplist 的字节大小
    unsigned int sz;
    // ziplist 的元素个数
    unsigned int count: 16;
    // 编码方式,『原生字节数组』或「压缩存储」
    unsigned int encoding: 2;
    // 存储方式,NONE==1 or ZIPLIST==2
    unsigned int container: 2;
    // 数据是否被压缩
    unsigned int recompress: 1;
    // 数据能否被压缩
    unsigned int attempted_compress: 1;
    // 预留的 bit 位
    unsigned int extra: 10;
} quicklistNode;

quicklist 的结构体定义如下:

typedef struct quicklist {
    // quicklist 的链表头
    quicklistNode *head;
    // quicklist 的链表尾
    quicklistNode *tail;
    // 所有 ziplist 中的总元素个数
    unsigned long count;
    // quicklistNodes 的个数
    unsigned long len;
    ……
} quicklist;

图示
在这里插入图片描述

typedef struct quicklistEntry {
    const quicklist *quicklist;
    quicklistNode *node;
    unsigned char *zi;
    unsigned char *value;
    long long longval;
    unsigned int sz;
    int offset;
} quicklistEntry;
quicklistCreate
quicklist *quicklistCreate(void) {
    struct quicklist *quicklist;

    quicklist = zmalloc(sizeof(*quicklist));
    quicklist->head = quicklist->tail = NULL;
    quicklist->len = 0;
    quicklist->count = 0;
    quicklist->compress = 0;
    quicklist->fill = -2;
    quicklist->bookmark_count = 0;
    return quicklist;
}

quicklistDelIndex 删除节点

REDIS_STATIC int quicklistDelIndex(quicklist *quicklist, quicklistNode *node,
                                   unsigned char **p) {
    int gone = 0;

    // 在该节点下的 ziplist 中删除
    node->zl = ziplistDelete(node->zl, p);
    // count-1
    node->count--;
    // ziplist 数量为空,直接删除该节点
    if (node->count == 0) {
        gone = 1;
        __quicklistDelNode(quicklist, node);
    } else {
        quicklistNodeUpdateSz(node);
    }
    // 更新所有 ziplist 中的总元素个数
    quicklist->count--;
    return gone ? 1 : 0;
}

quicklistDelEntry 删除节点

核心还是调用了 quicklistDelIndex,但是 quicklistDelEntry 要维护 quicklistNode 的节点,包括迭代器。

void quicklistDelEntry(quicklistIter *iter, quicklistEntry *entry) {
    quicklistNode *prev = entry->node->prev;
    quicklistNode *next = entry->node->next;
    int deleted_node = quicklistDelIndex((quicklist *)entry->quicklist,
                                         entry->node, &entry->zi);

    iter->zi = NULL;

    // 如果当前节点被删除,更新 iterator
    if (deleted_node) {
        if (iter->direction == AL_START_HEAD) {
            iter->current = next;
            iter->offset = 0;
        } else if (iter->direction == AL_START_TAIL) {
            iter->current = prev;
            iter->offset = -1;
        }
    }
}

quicklistInsertBefore, quicklistInsertAfter 前插和后插
插入分为两种:

void quicklistInsertAfter(quicklist *quicklist, quicklistEntry *node,
                          void *value, const size_t sz);
void quicklistInsertBefore(quicklist *quicklist, quicklistEntry *node,
                           void *value, const size_t sz);

其底层都调用了_quicklistInsert 函数:

void quicklistInsertBefore(quicklist *quicklist, quicklistEntry *entry,
                           void *value, const size_t sz) {
    _quicklistInsert(quicklist, entry, value, sz, 0);
}

void quicklistInsertAfter(quicklist *quicklist, quicklistEntry *entry,
                          void *value, const size_t sz) {
    _quicklistInsert(quicklist, entry, value, sz, 1);
}

_quicklistInsert 函数比较长,但是逻辑很简单,就是判断应该在哪里插入新元素:

在后面插入

当前 entry 的 ziplist 未满:直接插入
当前 entry 的 ziplist 已满:
entry->next 的 ziplist 未满:在其头部插入
entry->next 的 ziplist 已满:拆分当前 entry

在前面插入

当前 entry 的 ziplist 未满:直接插入
当前 entry 的 ziplist 已满:
entry->prev 的 ziplist 未满:在其尾部插入
entry->prev 的 ziplist 已满:拆分当前 entry

省略 _quicklistInsert函数实现。。。

listpack

listpack是什么?

紧凑列表,用一块连续的内存空间来紧凑保存数据,同时使用多种编码方式,表示不同长度的数据(字符串、整数)。

  1. 结构定义:listpack.h
  2. 实现:listpack.c

listpack数据结构

在这里插入图片描述
编码类型
在 listpack.c 文件中,有大量的 LP_ENCODING__XX_BIT_INT 和 LP_ENCODING__XX_BIT_STR 的宏定义:

#define LP_ENCODING_7BIT_UINT 0
#define LP_ENCODING_7BIT_UINT_MASK 0x80
#define LP_ENCODING_IS_7BIT_UINT(byte) (((byte)&LP_ENCODING_7BIT_UINT_MASK)==LP_ENCODING_7BIT_UINT)

#define LP_ENCODING_6BIT_STR 0x80
#define LP_ENCODING_6BIT_STR_MASK 0xC0
#define LP_ENCODING_IS_6BIT_STR(byte) (((byte)&LP_ENCODING_6BIT_STR_MASK)==LP_ENCODING_6BIT_STR)

……

listpack 元素会对不同长度的整数和字符串进行编码。

整数编码
以 LP_ENCODING_7BIT_UINT 为例,元素的实际数据是一个 7 bit 的无符号整数。

整数其余的编码方式有:

  • LP_ENCODING_16BIT_INT
  • LP_ENCODING_24BIT_INT
  • LP_ENCODING_32BIT_INT
  • LP_ENCODING_64BIT_INT

字符串编码

3 种类型:

  • LP_ENCODING_6BIT_STR
  • LP_ENCODING_12BIT_STR
  • LP_ENCODING_32BIT_STR

ziplist干啥去了?为什么有listpack?

ziplist是存储在连续内存空间,节省内存空间。quicklist是个节点为ziplist的双向链表,其通过控制quicklistNode结构里的压缩列表的大小或者元素个数,来减少连锁更新带来的性能影响,但这并没有完全解决连锁更新的问题。这是因为压缩列表连锁更新的问题来源于它的结构设计

从Redis 5.0版本开始,设计了一个新的数据结构叫做listpack ,目的是替代原来的压缩列表。在每个listpack节点中,不再保存前一个节点的长度,所以也就不存在出现连锁更新的情况了。

Redis7.0 才将 listpack 完整替代 ziplist。

什么是ziplist的连锁更新?

ziplist中元素个数多了,其查找效率就降低。若是在ziplist里新增或修改数据,ziplist占用的内存空间还需要重新分配;更糟糕的是,ziplist新增或修改元素时候,可能会导致后续元素的previous_entry_length占用空间都发生变化,从而引起连锁更新,导致每个元素的空间都需要重新分配,更加导致其访问性能下降。

listpack 如何避免连锁更新?

为了应对这些问题,Redis先是在3.0版本实现了quicklist结构。其是在ziplist基础上,使用链表将多个ziplist串联起来,即链表的每个元素是一个ziplist。这样可以减少数据插入时内存空间的重新分配及内存数据的拷贝。而且,quicklist也限制了每个节点上ziplist的大小,要是某个ziplist的元素个数多了,会采用新增节点的方法

但是因为quicklist使用节点结构指向了每个ziplist,这又增加了内存开销。而为了减少内存开销,并进一步避免ziplist连锁更新的问题,所以就有了listpack结构

listpack每个列表项都只记录自己的长度,不会像 ziplist 的列表项会记录前一项的长度。所以在 listpack 中新增或修改元素,只会涉及到列表项自身的操作,不会影响后续列表项的长度变化,进而避免连锁更新

listpack是沿用了ziplist紧凑型的内存布局。而listpack中每个节点不再包含前一节点的长度,所以当某个节点中的数据发生变化时候,导致节点的长度变化也不会影响到其他节点,这就可以避免连锁更新的问题了。

listpack相比ZipList的主要优势就在于解决了连锁更新的问题,提高了Redis的处理性能

listpack替代了quicklist吗?

在Redis 7.0版本之前,quicklist是由双向链表和压缩列表构成的。然而,在Redis 7.0版本中,quicklist的底层实现由双向链表和压缩列表变为了由双向链表和listpack构成的结构。

这表明,虽然listpack在Redis 7.0版本中被引入并用于替代压缩列表(ziplist)的部分功能,但quicklist的结构仍然存在,只是其中的压缩列表部分被listpack所替代。因此,listpack并没有完全替代quicklist,而是作为quicklist的一部分,改变了其原有的实现方式‌

在这里插入图片描述

标签:ENCODING,quicklistNode,Redis,ziplist,quicklist,源码,entry,listpack
From: https://blog.csdn.net/Larry_794204525/article/details/142268305

相关文章

  • 基于SpringBoot+Vue的影视剧评论系统 ---附源码 78088
    目  录摘要Abstract1绪论1.1研究背景与意义1.2国内外研究现状1.3论文结构与章节安排2 影视剧评论系统分析2.1可行性分析2.1.1技术可行性分析2.1.2经济可行性分析2.1.3法律可行性分析2.2系统功能分析2.2.1功能性分析2.3系统用例分析......
  • 基于Web宠物用品管理系统---附源码78346
    目录摘要1绪论1.1选题背景与意义1.2国内外研究现状1.3论文结构与章节安排2系统分析2.1可行性分析2.2系统流程分析2.2.1系统开发流程2.2.2用户登录流程2.2.3系统操作流程2.2.4添加信息流程2.2.5修改信息流程2.2.6删除信息流程2.3 系......
  • 基于SpringBoot的网上招聘系统的设计与实现---附源码72387
    目 录第1章引 言1.1选题背景1.2研究现状1.3论文结构安排第2章系统的需求分析2.1系统可行性分析2.1.1技术方面可行性分析2.1.2经济方面可行性分析2.1.3法律方面可行性分析2.1.4操作方面可行性分析2.2系统功能需求分析2.3系统性......
  • 基于微信小程序的高校体育场管理系统-计算机毕业设计源码+LW文档
    (一)本课题的目的通过本次课题能够将所学的软件开发知识应用于基于微信小程序的高校体育场管理系统的开发,完成基于微信小程序的高校体育场管理系统设计与实现。同时培养学生综合运用所学理论知识和技能;培养学生调查研究,查阅技术文献、资料、手册以及编写技术文档的能力;通过本次课题,......
  • 用于日语词汇学习的微信小程序-计算机毕业设计源码+LW文档
    摘要日语词汇学习小程序是高校人才培养计划的重要组成部分,是实现人才培养目标、培养学生科研能力与创新思维、检验学生综合素质与实践能力的重要手段与综合性实践教学环节。本学生所在学院多采用半手工管理日语词汇学习小程序的方式,所以有必要开发日语词汇学习小程序管理系统来对......
  • 基于微信的乐室预约小程序-计算机毕业设计源码+LW文档
    摘要随着社会的发展,社会的方方面面都在利用信息化时代的优势。互联网的优势和普及使得各种系统的开发成为必需。本文以实际运用为开发背景,运用软件工程原理和开发方法,它主要是采用java语言技术和mysql数据库来完成对系统的设计。整个开发过程首先对乐室预约小程序进行需求分析,得......
  • Java项目实战II基于Java+Spring Boot+MySQL的大学城水电管理系统(源码+数据库+文档)
    目录一、前言二、技术介绍三、系统实现四、论文参考五、核心代码六、源码获取全栈码农以及毕业设计实战开发,CSDN平台Java领域新星创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末一、前言前随着高等教育的快速发展,大学城作为集中......
  • Java项目实战II基于Java+Spring Boot+MySQL的大型商场应急预案管理系统(源码+数据库+
    目录一、前言二、技术介绍三、系统实现四、论文参考五、核心代码六、源码获取全栈码农以及毕业设计实战开发,CSDN平台Java领域新星创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末一、前言在快节奏的现代都市生活中,大型商场作为城......
  • 微信小程序的学生选课系统--论文源码调试讲解
    第二章开发技术介绍此次管理系统的关键技术和架构由B/S结构、java和mysql数据库,是本系统的关键开发技术,对系统的整体、数据库、功能模块、系统页面以及系统程序等设计进行了详细的研究与规划。2.1系统开发平台在该在线微信小程序的学生选课系统中,Eclipse能给用户提供更......
  • php医院预约挂号系统小程序 LW PPT源码调试讲解
    第二章开发技术与环境配置2.1Php语言简介PHP,原名HypertextPreprocessor。它是属于内嵌式语言,在服务器上执行嵌入HTML的脚本语言,有点像C语言的风格,运用的比较广泛。HypertextPreprocessor混合了Perl、C、Java和自己创新的语法。综合成比前者执行动态网页更快。与其他的......