Redis List 底层三种数据结构原理剖析

news2024/12/30 1:24:14

1. Redis List 是什么

作为 Java 开发者的你,看到这个词并不陌生。在 Java 开发中几乎每天都会使用这个数据结构。

Redis 的 List 与 Java 中的 LinkedList 类似,是一种线性的有序结构,可以按照元素被推入列表中的顺序来存储元素,能满足先进先出的需求,这些元素既可以是文字数据,又可以是二进制数据。

你可以把他当做队列、栈来使用。

2. 修炼心法

我叫 Redis,在 C 语言中,并没有现成的链表结构,所以 antirez 为我专门设计了一套实现方式。

关于 List 类型的底层数据结构,可谓英雄辈出,antirez 大佬一直在优化,创造了多种数据结构来保存。

从一开始早期版本使用 linkedlist(双端列表)ziplist(压缩列表)作为 List 的底层实现,到 Redis 3.2 引入了由 linkedlist + ziplist 组成的 quicklist,再到 7.0 版本的时候使用 listpack 取代 ziplist

MySQL:“为何弄了这么多数据结构呀?”

antirez 所做的这一切都是为了在内存空间开销与访问性能之间做取舍和平衡,跟着我去吃透每个类型的设计思想和不足,你就明白了。

linkedlist(双端列表)

在 Redis 3.2 版本之前,List 的底层数据结构由 linkedlist 或者 ziplist 实现,优先使用 ziplist 存储。

当列表对象满足以下两个条件的时候,List 将使用 ziplist 存储,否则使用 linkedlist。

  • List 的每个元素的占用的字节小于 64 字节。

  • List 的元素数量小于 512 个。

链表的节点使用 adlist.h/listNode结构来表示。

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

listNode 之间通过 prev 和 next 指针组成双端链表。除此之外,我还提供了 adlist.h/list 结构提供了头指针 head、尾指针 tail 以及一些实现多态的特定函数。

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

linkedlist 的结构如图 2-5 所示。

b25d6a9c4227add845acf08fb1092775.png
图 2-5

图 2-5

Redis 的链表实现的特性总结如下。

  • 双端:链表节点带有 prev 和 next 指针,获取某个节点的前置节点和后继节点的复杂度都是 O(1)。

  • 无环:表头节点的 prev 指针和尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为结束。

  • 带表头指针和表尾指针:通过 list 结构的 head 指针和 tail 指针,程序获取链表的头节点和尾节点的复杂度为 O(1)。

  • 使用 list 结构的 len 属性来对记录节点数量,获取链表中节点数量的复杂度为 O(1)。

MySQL:“看起来没啥问题呀,为啥还要 ziplist 呢?”

你知道的,我在追求快和节省内存的方向上无所不及,有两个原因导致了 ziplist 的诞生。

  • 普通的 linkedlist 有 prev、next 两个指针,当存储数据很小的情况下,指针占用的空间会超过数据占用的空间,这就离谱了,是可忍孰不可忍。

  • linkedlist 是链表结构,在内存中不是连续的,遍历的效率低下。

ziplist(压缩列表)

为了解决上面两个问题,antirez 创造了 ziplist 压缩列表,是一种内存紧凑的数据结构,占用一块连续的内存空间,提升内存使用率。

当一个列表只有少量数据的时候,并且每个列表项要么是小整数值,要么就是长度比较短的字符串,那么我就会使用 ziplist 来做 List 的底层实现。

ziplist 中可以包含多个 entry 节点,每个节点可以存放整数或者字符串,结构如图 2-6 所示。

97d9410c3cc611d08d2f6024e8dcd146.png
图 2-6

图 2-6

  • zlbytes,占用 4 个字节,记录了整个 ziplist 占用的总字节数。

  • zltail,占用 4 个字节,指向最后一个 entry 偏移量,用于快速定位最后一个 entry。

  • zllen,占用 2 字节,记录 entry 总数。

  • entry,列表元素。

  • zlend,ziplist 结束标志,占用 1 字节,值等于 255。

因为 ziplist 头尾元数据的大小是固定的,并且在 ziplist 头部 zllen 记录了最后一个元素的位置,所以,当在 ziplist 中查找第一个或最后一个元素的时候,能以 O(1) 时间复杂度找到。

而查找中间元素时,只能从列表头或者列表尾遍历,时间复杂度就是 O(N)。

接下来看真正存储数据的 entry 结构长啥样。

2863b6251461fa3a1e8ef8e2502b238e.png
图 2-7

图 2-7

正常来说有三部分构成 <prevlen> <encoding> <entry-data>

prevlen

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

这部分会根据上一个 entry 的长度进行变长编码(为了节省内存操碎了心),变长方式如下。

  • 前一个 entry 的字节大小小于 254(255 用于 zlend),prevlen 长度为 1 字节,值等于上一个 entry 的长度。

  • 前一个 entry 的字节大小大于等于 254,prevlen 占用 5 字节,第一个字节设置为 254 作为一个标识,后面四字节组成一个 32 位的 int 值,用于存放上一个 entry 的字节长度。

encoding

简言之用于表示当前 entry 的类型和长度,当前 entry 的长度和值是根据保存的是 int 还是 string 以及数据的长度共同来决定。

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

entry-data

实际存放数据的区域,需要注意的是,如果 entry 中存储的是 int 类型,encoding 和 entry-data 会合并到 encoding 中,没有 entry-data 字段。

此刻结构就变成了 <prevlen> <encoding>

MySQL:“为什么说 ziplist 省内存?”

  1. 与 linkedlist 相比,少了 prev、next 指针。

  2. 通过 encoding 字段针对不同编码来细化存储,尽可能做到按需分配,当 entry 存储的是 int 类型时,encoding 和 entry-data 会合并到 encoding ,省掉了 entry-data 字段。

  3. 每个 entry-data 占据内存大小不一样,为了解决遍历问题,增加了 prevlen 记录上一个 entry 长度。遍历数据时间复杂度是 O(1),但是数据量很小的情况下影响不大。

MySQL:“听起来很完美,为啥还搞什么 quicklist ”

既要又要还要的需求是很难实现的,ziplist 节省了内存,但是也有不足。

  • 不能保存过多的元素,否则查询性能会大大降低,O(N) 时间复杂度。

  • ziplist 存储空间是连续的,当插入新的 entry 时,内存空间不足就需要重新分配一块连续的内存空间,引发连锁更新的问题。

连锁更新

每个 entry 都用 prevlen 记录了上一个 entry 的长度,从当前 entry B 前面插入一个新的 entry A 时,会导致 B 的 prevlen 改变,也会导致 entry B 大小发生变化。entry B 后一个 entry C 的 prevlen 也需要改变。以此类推,就可能造成了连锁更新。

8103f0d5aa19448826ed596483591423.png
图 2-8

图 2-8

连锁更新会导致 ziplist 的内存空间需要多次重新分配,直接影响 ziplist 的查询性能。于是乎在 Redis 3.2 版本引入了 quicklist。

quicklist

quicklist 是综合考虑了时间效率与空间效率引入的新型数据结构。结合了原先 linkedlist 与 ziplist 各自的优势,本质还是一个链表,只不过链表的每个节点是一个 ziplist。

数据结构定义在 quicklist.h 文件中,链表由 quicklist 结构体定义,每个节点由 quicklistNode 结构体定义(源码版本为 6.2,7.0 版本使用 listpack 取代了 ziplist)。

quicklist 是一个双向链表,所以每个 quicklistNode 都有前序指针(*prev)、后序指针(*next)。每个节点是 ziplist,所以还有一个指向 ziplist 的指针 *zl

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 作为链表,定义了 头、尾指针,用于快速定位表表头和链表尾。

typedef struct quicklist {
    // 链表头指针
    quicklistNode *head;
    // 链表尾指针
    quicklistNode *tail;
    // 所有 ziplist 的总 entry 个数
    unsigned long count;
    // quicklistNode 个数
    unsigned long len;
    int fill : QL_FILL_BITS;
    unsigned int compress : QL_COMP_BITS;
    unsigned int bookmark_count: QL_BM_BITS;
    // 柔性数组,给节点添加标签,通过名称定位节点,实现随机访问的效果
    quicklistBookmark bookmarks[];
} quicklist;

结合 quicklist 和 quicklistNode定义,quicklist 链表结构如下图所示。

288f457d893c1d8df8975c5f3e3fc0a5.png
图 2-9

图 2-9

从结构上看,quicklist 就是 ziplist 的升级版,优化的关键点在于控制好每个 ziplist 的大小或者元素个数。

  • quicklistNode 的 ziplist 越小,可能会造成更多的内存碎片,极端情况下是每个 ziplist 只有一个 entry,退化成了 linkedlist。

  • quicklistNode 的 ziplist 过大,极端情况下一个 quicklist 只有一个 ziplist,退化成了 ziplist。连锁更新的性能问题就会暴露无遗。

合理配置很重要,Redis 提供了 list-max-ziplist-size -2

list-max-ziplist-size 为负数时表示限制每个 quicklistNode 的 ziplist 的内存大小,超过这个大小就会使用 linkedlist 存储数据,每个值有以下含义:

  • -5:每个 quicklist 节点上的 ziplist 大小最大 64 kb <--- 正常环境不推荐

  • -4:每个 quicklist 节点上的 ziplist 大小最大 32 kb <--- 不推荐

  • -3:每个 quicklist 节点上的 ziplist 大小最大 16 kb <--- 可能不推荐

  • -2:每个 quicklist 节点上的 ziplist 大小最大 8 kb <--- 不错

  • -1:每个 quicklist 节点上的 ziplist 大小最大 4kb <--- 不错

默认值为 -2,也是官方最推荐的值,当然你可以根据自己的实际情况进行修改。

MySQL:“搞了半天还是没能解决连锁更新的问题嘛”

别急,饭要一口口吃,路要一步步走,步子迈大了容易扯着蛋。

ziplist 是紧凑型数据结构,可以有效利用内存。但是每个 entry 都用 prevlen 保留了上一个 entry 的长度,所以在插入或者更新时可能会出现连锁更新影响效率。

于是 antirez 又设计出了“链表 + ziplist” 组成的 quicklist 来避免单个 ziplist 过大,降低连锁更新的影响范围。

可毕竟还是使用了 ziplist,本质上无法避免连锁更新的问题,于是乎在 5.0 版本设计出另一个内存紧凑型数据结构 listpack,于 7.0 版本替换掉 ziplist。

listpack

出现 listpack 的原因是因为用户上报了一个 Redis 崩溃的问题,但是 antirez 并没有找到崩溃的明确原因,猜测可能是 ziplist 结构导致的连锁更新导致的,于是就想设计一种简单、高效的数据结构来替换 ziplist 这个数据结构。

MySQL:“listpack 是啥?”

listpack 也是一种紧凑型数据结构,用一块连续的内存空间来保存数据,并且使用多种编码方式来表示不同长度的数据来节省内存空间。

源码文件 listpack.h对 listpack 的解释:A lists of strings serialization format,意思是一种字符串列表的序列化格式,可以把字符串列表进行序列化存储,可以存储字符串或者整形数字。

先看 listpack 的整体结构。

9c1a31b77d43063ec4427485b8ce3f56.png
图 2-10

图 2-10

一共四部分组成,tot-bytes、num-elements、elements、listpack-end-byte。

  • tot-bytes,也就是 total bytes,占用 4 字节,记录 listpack 占用的总字节数。

  • num-elements,占用 2 字节,记录 listpack elements 元素个数。

  • elements,listpack 元素,保存数据的部分。

  • listpack-end-byte,结束标志,占用 1 字节,值固定为 255。

MySQL:“好家伙,这跟 ziplist 有啥区别?别以为换了个名字,换个马甲我就不认识了”

听我说完!确实有点像,listpack 也是由元数据和数据自身组成。最大的区别是 elements 部分,为了解决 ziplist 连锁更新的问题,element 不再像 ziplist 的 entry 保存前一项的长度

343085b8ad98144bca536893be6ea553.png
图 2-11

图 2-11

  • encoding-type,元素的编码类型,会不同长度的整数和字符串编码。

  • element-data,实际存放的数据。

  • element-tot-len,encoding-type + element-data 的总长度,不包含自己的长度。

每个 element 只记录自己的长度,不像 ziplist 的 entry,记录上一项的长度。当修改或者新增元素的时候,不会影响后续 element 的长度变化,解决了连锁更新的问题。

linkedlistziplist 到“链表 + ziplist” 构成的 quicklist,再到 listpack 结构。可以看到,设计的初衷都是能够高效的使用内存,同时避免性能下降。

点击下方卡片关注「码哥字节」,只写干货的硬核男人

点击上方“码哥字节”,选择“设为星标” ,优质资源及时送达

另外,码哥专属微信号如下,坑位有限,欲加从速。

45630896daf12f8dba21e903a8a33d6f.jpeg

最后,希望你点赞,关注,转发。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/392610.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023年第九周总周结 | 开学第一周

为什么要做周总结&#xff1f; 1.避免跳相似的坑 2.客观了解上周学习进度并反思&#xff0c;制定可完成的下周规划 一、上周问题解决情况 不满却又喜欢“受害者”身份项目导向学习进展困难&#xff0c;进而产生挫败焦虑作息调整→学习时长变少and变碎 二、这周存在问题 and 反…

Tomcat 配置文件数据库密码加密

几年前研究过Tomcat context.xml 中数据库密码改为密文的内容&#xff0c;因为当时在客户云桌面代码没有留备份也没有文章记录&#xff0c;最近项目又提出了这个需求就又重新拾起来学习一下。在网上找了一些资料&#xff0c;自己也大概试了一下&#xff0c;目前功能是实现了。参…

SpringCloud系列(十五)[分布式搜索引擎篇] - 结合实际应用场景学习并使用 RestClient 客户端 API

前面的文章具体介绍了是索引库及文档的一些基本操作指令, 指令还是挺简单的; 那么实际应用场景下, 我们是如何操作 ElasticSearch 的呢?  其实 ElasticSearch 官方已经为我们提供了各种不同语言的客户端, 目的就是为了来操作 ElasticSearch, 这些客户端的本质就是组装 DSL 语…

vmware虚拟机与树莓派4B安装ubuntu1804 + ros遇到的问题

如题所示&#xff0c;本人在虚拟机上安装ubuntu1804&#xff0c;可以很容易安装&#xff0c;并且更换系统apt源和ros源&#xff0c;然后安装ros&#xff0c;非常顺利&#xff0c;但是在树莓派4B上安装raspiberry系统就遇到了好多问题。 树莓派我烧录的是这个镜像&#xff1a;ub…

ASO优化之选择最佳关键词

ASO的关键字排名是指针对特定的关键词在应用商店搜索结果中所形成的位置。虽然这看起来很简单&#xff0c;但应用商店排名不仅仅是位置&#xff0c;应用的排名统计数据都要考虑进去。 应用商店搜索结果因国家/地区而异&#xff0c;这就意味着如果我们从不同的国家或地区进行搜…

经典文献阅读之--Lifelong SLAM(变化环境中Lifelong定位建图)

0. 简介 商场、超市等大多数现实场景的环境随时都在变化。不考虑这些变化的预建地图很容易变得过时。因此&#xff0c;有必要拥有一个最新的环境模型&#xff0c;以促进机器人的长期运行。为此《A General Framework for Lifelong Localization and Mapping in Changing Envir…

Oracle技术分享 exp导数据时报错ORA-01578 ORA-01110

问题描述&#xff1a;exp导数据时报错ORA-01578 ORA-01110&#xff0c;如下所示&#xff1a; 数据库&#xff1a;oracle 19.12 多租户 1、异常重现 [oracledbserver ~]$ exp ora1/ora1orclpdbfileemp.dmp tablesemp logexp.log Export: Release 19.0.0.0.0 - Production onS…

OpenEuler20.03源码安装配置PostgreSQL13.4详细图文版

OpenEuler安装配置PostgreSQL 序号更新内容更新日期更新人1完成第一至三章内容编辑&#xff1b;2021年9月18日liupp2增加PostgreSQL服务开机自动启动&#xff1b;2021年10月25日liupp 一、准备条件 OpenEuler(Hyper-V虚拟机)&#xff1a; 版本&#xff1a;20.03 LTS SP2下载地…

推荐几个超实用的开源自动化测试框架

有什么好的开源自动化测试框架可以推荐&#xff1f;为了让大家看文章不蒙圈&#xff0c;文章我将围绕3个方面来阐述&#xff1a; 1、通用自动化测试框架介绍 2、Java语言下的自动化测试框架 3、Python语言下的自动化测试框架 随着计算机技术人员的大量增加&#xff0c;通过编写…

什么是MyBatis?无论是基础教学还是技术精进,你都应该看这篇MyBatis

文章目录学习之前&#xff0c;跟你们说点事情&#xff0c;有助于你能快速看完文章一、先应用再学习&#xff0c;代码示例1. 第一个MyBatis程序2. MyBatis整合Spring3. SpringBoot整合MyBatis二、MyBatis整体流程&#xff0c;各组件的作用域和生命周期三、说说MyBatis-config.xm…

流域土壤保持及GIS实现

流域土壤保持及GIS实现 流域水土过程模拟与生态调控 01 土壤保持模拟 土壤侵蚀不仅会引起耕地生产力下降、河床抬升、泥沙淤积阻塞河道等生态环境问题&#xff0c;也会对人们正常的生产生活产生威胁。生态系统的土壤保持量&#xff08;吨/公顷/年&#xff09;&#xff0c;是…

$3 : 水​​​​​项目实战 - 水果库存系统

javase知识点复习&#xff1a; final关键字&#xff1a;http://t.csdn.cn/bvFgu 接口的定义&#xff0c;特性&#xff0c;实现&#xff0c;继承&#xff1a;http://t.csdn.cn/tbXl3 异常&#xff1a;http://t.csdn.cn/VlS0Z DAO的概念和角色&#xff08;设计理念&#xff09;&a…

适配PyTorch FX,OneFlow让量化感知训练更简单

作者 | 刘耀辉审稿 | BBuf、许啸宇1背景近年来&#xff0c;量化感知训练是一个较为热点的问题&#xff0c;可以大大优化量化后训练造成精度损失的问题&#xff0c;使得训练过程更加高效。Torch.fx在这一问题上走在了前列&#xff0c;使用纯Python语言实现了对于Torch.nn.Module…

学习笔记:Java并发编程(补)CompletableFuture

学习视频&#xff1a;https://www.bilibili.com/video/BV1ar4y1x727 参考书籍&#xff1a;《实战 JAVA 高并发程序设计》 葛一鸣 著 系列目录 学习笔记&#xff1a;Java 并发编程①_基础知识入门学习笔记&#xff1a;Java 并发编程②_共享模型之管程学习笔记&#xff1a;Java 并…

win10开机黑屏只有鼠标怎么办?这里有4个妙招

真实案例&#xff1a;电脑开机黑屏&#xff0c;只出现鼠标箭头光标怎么办&#xff1f; “早上打开电脑&#xff0c;发现开不了机&#xff0c;屏幕上只有一个鼠标光标&#xff01;百度搜索了很长时间&#xff0c;但所有的方法都没有奏效。求教各位大神&#xff0c;有什么好方法…

中电金信源启小程序开发平台 赋能金融+业务生态共享共建

导语&#xff1a;源启小程序开发平台立足于“为金融业定制”&#xff0c;从小程序全生命周期的角度出发&#xff0c;助力银行、互联网金融、保险、证券客户实现一站式小程序开发、发布、运营与营销。企业可以通过源启小程序开发平台&#xff0c;低成本高效率开发一款定制化小程…

The 19th Zhejiang Provincial Collegiate Programming Contest vp

和队友冲了这场&#xff0c;极限6题&#xff0c;重罚时铁首怎么说&#xff0c;前面的A题我贡献了太多的罚时&#xff0c;然后我的G题最短路调了一万年&#xff0c;因为太久没写了&#xff0c;甚至把队列打成了优先队列&#xff0c;没把head数组清空完全&#xff0c;都是我的锅呜…

搭载英伟达Jetson Orin的Allspark 2全新亮相,算力高达100TOPS!

Allspark 2 系列AI边缘计算机 Allspark 2经过设计优化的铝合金外壳&#xff0c;内置静音涡轮风扇&#xff0c;散热优秀。尺寸102.5X62.5X31mm&#xff0c;整机重量188g。 相比Allspark 1&#xff0c;2代整机轻了25克&#xff0c;更加轻薄。 在机身更加轻薄的情况下&#xff0c…

1497. 树的遍历

文章目录1.二叉树的遍历2.二叉树的构造3.例题3.1不使用BFS3.2使用BFS二叉树的构造&#xff1a;没有中序遍历则无法唯一构造1.二叉树的遍历 2.二叉树的构造 3.例题 一个二叉树&#xff0c;树中每个节点的权值互不相同。 现在给出它的后序遍历和中序遍历&#xff0c;请你输出它…

蓝桥杯训练day2

day21.二分(1)789. 数的范围(2)四平方和&#xff08;1&#xff09;哈希表做法&#xff08;2&#xff09;二分做法(3)1227. 分巧克力&#xff08;4&#xff09;113. 特殊排序(5)1460. 我在哪&#xff1f;2.双指针(1)1238. 日志统计(2)1240. 完全二叉树的权值&#xff08;3&#…