0

0

redis学习笔记-list原理

Golang菜鸟

Golang菜鸟

发布时间:2023-08-07 16:36:14

|

1592人浏览过

|

来源于Golang菜鸟

转载

通义听悟
通义听悟

阿里云通义听悟是聚焦音视频内容的工作学习AI助手,依托大模型,帮助用户记录、整理和分析音视频内容,体验用大模型做音视频笔记、整理会议记录。

下载

list 基本功能

命令 描述
blpop key1,key2,…… timeout 移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或者弹出元素为止。
brpop key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
brpoplpush source destination timeout 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它;如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
lindex key index 通过索引获取列表中的元素
linsert key before/after pivot value 在列表的元素前或者后插入元素
llen key 获取列表长度
lpop key 移出并获取列表的第一个元素
lpush key value1,value2,… 将一个或者多个值插入到列表头部
lpushx key value 将一个值插入到已经存在的列表头部
lrange key srart stop 获取列表指定范围内的元素
lrem key count value 移除列表元素
lset key index value 通过索引设置列表元素的值
ltrim key start stop 对一个列表进行修剪,就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都被删除。index从0开始,区间均包含。
rpop key 移除列表的最后一个元素,返回值为移除的元素
rpoppush source destination 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
rpush key value1 value2 …… 添加一个或者多个值到list的尾部
rpushx key value 为已经存在的;列表添加值

图表来自:https://www.cnblogs.com/amyzhu/p/13466311.html

单链表

在学习 redis 的 list 实现之前,我们先来看一下单链表 list 怎么实现:

每一个节点都有一个向后的指针(引用)指向下一个节点,最后一个节点指向NULL表示结束,有一个Head(头)指针指向第一个节点表示开始。

redis学习笔记-list原理

类似于这样,新建和删除虽然只需要 O(1),但是查找需要 O(n) 的时间;无法反向查找,一旦错过需要从头开始。

增加一个节点:

redis学习笔记-list原理

删除一个节点:

redis学习笔记-list原理

双向链表

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

redis学习笔记-list原理

特点:

  1. 每次在插入或删除某个节点时, 需要处理四个节点的引用, 而不是两个. 实现起来要困难一些

  2. 相对于单向链表, 必然占用内存空间更大一些.

  3. 既可以从头遍历到尾, 又可以从尾遍历到头

这好像就解决 redis 可以实现前后都可以遍历的问题了。

那么我们来看看 redis 的链表 怎么处理的:

redis学习笔记-list原理

再来看一下它的结构体定义源码

ListNode:

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

List:

typedef struct listNode
{
    // 表头节点
    listNode *head;
    // 表尾节点
    listNode *tail;
    // 链表所包含的节点数量
    unsigned long len;
    // 节点值复制函数
    void *(*dup)(void *ptr);
    // 节点值释放函数
    void *(*free)(void *ptr);
    // 节点值对比函数
    int (*match)(void *ptr,void *key)
} list;

redis 链表的特性:

  • 双向无环:链表节点带有前驱、后继指针获取某个节点的前驱、后继节点的时间复杂度为O(1),表头节点的前驱指针和表尾节点的后继指针都指向NULL,对链表的访问以NULL为终点。

  • 长度计数器:通过List结构的len属性获取节点数量的时间复杂度为O(1)。

由于 list 还存在一个内存分配不连续,并引发的内存碎片的问题,是否有办法让他们的内存优化一下?

redis 压缩列表

ZipList不是基础数据结构,是Redis自己设计的一种数据存储结构。它有点类似数组,通过一片连续的内存空间来存储数据。

与数组不同的是,它允许所存储的列表元素所占据的内存空间不同。说到压缩这两个字,大家第一时间可能想到的就是节省内存。之所以说这种存储结构节省内存,是相对数组而言的。

我们都知道,数组要求每个元素存储空间的大小相同,如果我们要存储不同长度的字符串,就要以最大长度的字符串所占的存储空间作为字符串数组每个元素存储空间的大小(假如是50字节)。

因此在字符数数值中存储小于50字节长度的字符串时就会浪费一部分存储空间。

数组 的优势就是占用一片连续的空间,可以很好地利用CPU缓存快速访问数据。

如果既想保留数组的这种优势又想节省存储空间,那么我们可以对数组进行压缩:

redis学习笔记-list原理

不过,有一个问题,在遍历压缩列表的时候我们并不知道每个元素占用的内存大小是多少,因此无法计算出下一个元素的具体起始位置。

但是转念一想,如果每个元素访问之前我们能有它的长度,那么不就可以解决这个问题了嘛。

redis学习笔记-list原理

接下来我们看看Redis是如何通过实现ZipList使之结合既保留了数组的优点又节省了内存。

redis学习笔记-list原理

  • zlbytes:压缩列表的字节长度,是uint32_t类型,占4字节,因此压缩列表最多有232-1字节,可以通过该值计算zlend的位置,也可以在内存重新分配的时候使用。

  • zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量,是uint32_t类型,占4字节,可以通过该值快速定位到列表尾元素的位置。

  • zllen:压缩列表元素的个数,是uint16_t类型,占2字节,表示最多存储的元素个数为216-1=65 535,如果需要计算总数量,则需要遍历整个压缩列表。

  • entryx:压缩列表存储的元素,既可以是字节数组,也可以是整数,长度不限。

  • zlend:压缩列表的结尾,是uint8_t类型,占1字节,恒为0xFF。

  • previous_entry_length:表示前一个元素的字节长度,占1字节或者5字节,当前元素的长度小于254时用1字节,大于等于254时用5字节,previous_entry_length 字段的第一个字节固定是0xFE,后面的4字节才是真正的前一个元素的长度。

  • encoding:表示元素当前的编码,有整数或者字节数。为了节省内存,encoding字段长度可变。

  • content:表示当前元素的内容。

ZipList变量的读取和赋值都是通过宏来实现的,代码片段如下:

//返回整个压缩列表的总字节
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))

//返回压缩列表的tail_offset变量,方便获取最后一个节点的位置
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))

//返回压缩列表的节点数量
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))

//返回压缩列表的表头的字节数
//(内存字节数zlbytes,最后一个节点地址ztail_offset,节点总数量zllength)
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))

//返回压缩列表最后结尾的字节数
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))

//返回压缩列表首节点地址
#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)

//返回压缩列表尾节点地址
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))

//返回压缩列表最后结尾的地址
#define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)


对此,可以通过定义的结构体和对应的操作定义知道大概 redis 设计 压缩list 的思路;

这种方式虽然节约了空间,但是会存在每次插入和创建存在内存重新分配的问题。

quicklist

quicklist是Redis 3.2中新引入的数据结构,能够在时间效率和空间效率间实现较好的折中。Redis中对quciklist的注释为A doubly linked list of ziplists。顾名思义,quicklist是一个双向链表,链表中的每个节点是一个ziplist结构。quicklist可以看成是用双向链表将若干小型的ziplist连接到一起组成的一种数据结构。

当ziplist节点个数过多,quicklist退化为双向链表,一个极端的情况就是每个ziplist节点只包含一个entry,即只有一个元素。当ziplist元素个数过少时,quicklist可退化为ziplist,一种极端的情况就是quicklist中只有一个ziplist节点。

redis学习笔记-list原理

redis学习笔记-list原理


quicklist 结构定义:

typedef struct quicklist {
    // 指向quicklist的首节点
    quicklistNode *head;
    // 指向quicklist的尾节点
    quicklistNode *tail;
    // quicklist中元素总数
    unsigned long count;        /* total count of all entries in all ziplists */
    // quicklistNode节点个数
    unsigned long len;          /* number of quicklistNodes */
    // ziplist大小设置,存放list-max-ziplist-size参数的值
    int fill : 16;              /* fill factor for individual nodes */
    // 节点压缩深度设置,存放list-compress-depth参数的值
    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
    unsigned int bookmark_count: 4;
    quicklistBookmark bookmarks[];
} quicklist;

typedef struct quicklistBookmark {
    quicklistNode *node;
    char *name;
} quicklistBookmark;

quicklistNode定义如下:

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

redis为了节省内存空间,会将quicklist的节点用LZF压缩后存储,但这里不是全部压缩,可以配置compress的值,compress为0表示所有节点都不压缩,否则就表示从两端开始有多少个节点不压缩;compress如果是1,表示从两端开始,有1个节点不做LZF压缩。compress默认是0(不压缩),具体可以根据你们业务实际使用场景去配置。

redis 的配置文件可以配置该参数

list-compress-depth 0

含义
0 特殊值,表示都不压缩
1 quicklist两端各有1个节点不压缩,中间的节点压缩
2 quicklist两端各有2个节点不压缩,中间的节点压缩
n quicklist两端各有n个节点不压缩,中间的节点压缩


还有个fill字段,它的含义是每个quicknode的节点最大容量,不同的数值有不同的含义,默认是-2,当然也可以配置为其他数值;

list-max-ziplist-size -2

  • 当值为正数时,表示quicklistNode节点上的ziplist的长度。比如当这个值为5时,每个quicklistNode节点的ziplist最多包含5个数据项

  • 当值为负数时,表示按照字节数来限制quicklistNode节点上的ziplist的的长度,可选值为 -1 到 -5。

含义
-1 ziplist节点最大为4kb
-2 ziplist节点最大为8kb
-3 ziplist节点最大为16kb
-4 ziplist节点最大为32kb
-5 ziplist节点最大为64kb

为什么会有配置提供出来呢?

  • ziplist越短,内存碎片增多,影响存储效率。当一个ziplist只存一个元素时,quicklist又退化成双向链表了

  • ziplist越长,为ziplist分配大的连续的内存空间难度也就越大,会造成很多小块的内存空间被浪费,当quicklist只有一个节点,元素都存在一个ziplist上时,quicklist又退化成ziplist了

结束语

虽然没有完全去理解它的源码,但我们也可以通过这篇文章来熟悉 redis 的一个设计思路。并知道它是怎么一步一步优化过来的。让我们有一个大概的性能认知。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

254

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

1089

2024.03.01

counta和count的区别
counta和count的区别

Count函数用于计算指定范围内数字的个数,而CountA函数用于计算指定范围内非空单元格的个数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

203

2023.11.20

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

760

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

221

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1566

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

649

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

1228

2024.03.22

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

3

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
进程与SOCKET
进程与SOCKET

共6课时 | 0.4万人学习

Redis+MySQL数据库面试教程
Redis+MySQL数据库面试教程

共72课时 | 7.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号