Redis设计与实现之双端链表

目录

一、Redis为什么选择双端链表作为底层数据结构?

二、双端链表

1、双端链表的应用

实现Redis的列表类型

Note: Redis列表使用两种数据结构作为底层实现:

Redis自身功能的构建

2、双端链表的实现

​编辑3、迭代器

三、双端链表在Redis中的应用场景有哪些?

四、双端链表在Redis中如何实现数据的插入和删除操作?

五、Redis双端链表与传统的单向链表有什么区别?

六、Redis双端链表在高并发情况下可能会出现什么问题?如何解决这些问题?

七、Redis双端链表如何处理内存分配和回收的问题?

八、Redis是否支持链表的排序操作?如何实现链表的排序?

九、Redis双端链表如何处理数据的重复问题?

十、Redis双端链表在持久化时如何处理?

十一、小结


一、Redis为什么选择双端链表作为底层数据结构?

Redis选择双端链表作为底层数据结构有以下几个原因:

  1. 高效的插入和删除操作:双端链表可以在常数时间复杂度O(1)内完成节点的插入和删除操作。这对于Redis的一些操作,比如列表(List)的push和pop操作,非常高效。

  2. 集合类型的操作:双端链表可以支持集合操作,比如求交集、并集和差集等。这对于Redis的一些操作,比如集合(Set)的操作,非常有用。

  3. 排序功能:双端链表可以支持排序功能,这对于Redis的有序集合(Sorted Set)非常重要。双端链表可以通过额外的数据结构(比如跳跃表)实现高效的排序操作。

  4. 内存紧凑:双端链表在内存上的占用比较紧凑,因为它只需要存储指向前后节点的指针。这对于Redis的内存管理和存储效率非常重要。

综上所述,双端链表作为底层数据结构,能够满足Redis在插入、删除、集合操作和排序等方面的需求,并且具有较高的存储效率。

二、双端链表

链表作为数组之外的一种常用序列抽象,是大多数高级语言的基本数据类型,因为 C 语言本身不支持链表类型,大部分 C 程序都会自己实现一种链表类型,Redis 也不例外——它实现了一 个双端链表结构。

双端链表作为一种常见的数据结构,在大部分的数据结构或者算法书里都有讲解,因此,这一 章关注的是 Redis 双端链表的具体实现,以及该实现的 API ,而对于双端链表本身,以及双端 链表所对应的算法,则不做任何解释。

另外,一些书籍,比如 《算法: C语言实现》 和《数据结构与算法分析》 则提供了关于双端链表的更详细的信息。

1、双端链表的应用

双端链表作为一种通用的数据结构,在 Redis内部使用得非常多:它既是 Redis列表结构的底层实现之一,还被大量 Redis模块所使用,用于构建 Redis的其他功能。

实现Redis的列表类型

双端链表还是 Redis列表类型的底层实现之一,当对列表类型的键进行操作——比如执行RPUSH 、LPOP或 LLEN等命令时,程序在底层操作的可能就是双端链表。

redis>RPUSH brands Apple Microsoft Google(integer) 3redis>LPOP brands"Apple"redis>LLEN brands(integer) 2redis>LRANGE brands 0-11)"Microsoft "2)"Google"

Note: Redis列表使用两种数据结构作为底层实现:

1.双端链表

2.压缩列表

因为双端链表占用的内存比压缩列表要多,所以当创建新的列表键时,列表会优先考虑使用压

缩列表作为底层实现,并且在有需要的时候,才从压缩列表实现转换到双端链表实现。

Redis自身功能的构建

除了实现列表类型以外,双端链表还被很多 Redis内部模块所应用:

•事务模块使用双端链表来按顺序保存输入的命令;

•订阅 /发送模块使用双端链表来保存订阅模式的多个客户端;

•事件模块使用双端链表来保存时间事件( time event ) ;

类似的应用还有很多,在后续的章节中我们将看到,双端链表在 Redis中发挥着重要的作用。

2、双端链表的实现

双端链表的实现由 listNode 和list两个数据结构构成,下图展示了由这两个结构组成的一个双端链表实例:

其中,listNode 是双端链表的节点:

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

而 list 则是双端链表本身:

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

注意,listNode 的 value 属性的类型是 void * ,说明这个双端链表对节点所保存的值的类型 不做限制。

对于不同类型的值,有时候需要不同的函数来处理这些值,因此,list 类型保留了三个函数指针——dup 、free 和 match ,分别用于处理值的复制、释放和对比匹配。在对节点的值进行处理时,如果有给定这些函数,那么它们就会被调用。

举个例子:当删除一个 listNode 时,如果包含这个节点的 list 的 list->free 函数不为空, 那么删除函数就会先调用 list->free(listNode->value) 清空节点的值,再执行余下的删除 操作(比如说,释放节点)。

另外,从这两个数据结构的定义上,也可以它们的一些行为和性能特征:

  • listNode 带有 prev 和 next 两个指针,因此,对链表的遍历可以在两个方向上进行:从 表头到表尾,或者从表尾到表头。

  • list 保存了 head 和 tail 两个指针,因此,对链表的表头和表尾进行插入的复杂度都为 θ(1) ——这是高效实现 LPUSH 、RPOP 、RPOPLPUSH 等命令的关键。

  • list 带有保存节点数量的 len 属性,所以计算链表长度的复杂度仅为 θ(1) ,这也保证 了 LLEN 命令不会成为性能瓶颈。

    以下是用于操作双端链表的 API ,它们的作用以及算法复杂度:

3、迭代器

Redis 为双端链表实现了一个迭代器 ,这个迭代器可以从两个方向对双端链表进行迭代: • 沿着节点的next指针前进,从表头向表尾迭代;
• 沿着节点的prev指针前进,从表尾向表头迭代;

以下是迭代器的数据结构定义:

typedef struct listIter { // 下一节点listNode *next;// 迭代方向 int direction;
} listIter;

direction 记录迭代应该从那里开始:
• 如果值为adlist.h/AL_START_HEAD,那么迭代器执行从表头到表尾的迭代; • 如果值为adlist.h/AL_START_TAIL,那么迭代器执行从表尾到表头的迭代;

以下是迭代器的操作 API ,它们的作用以及算法复杂度:

三、双端链表在Redis中的应用场景有哪些?

在Redis中,双端链表(Doubly Linked List)常用于以下场景:

  1. 列表结构:Redis提供了List数据类型,它实际上就是通过双端链表实现的。List可以在两端进行元素的添加和删除操作,可以用于实现队列(FIFO)或栈(LIFO)等数据结构。

  2. 发布与订阅:Redis的发布与订阅功能使用了双端链表来保存订阅者列表。当有新消息发布时,Redis会遍历链表,并将消息发送给每个订阅者。

  3. LRU缓存策略:Redis的LRU(Least Recently Used)缓存策略使用了双端链表来保存最近访问的键。当缓存空间不足时,Redis会删除链表尾部的键,以便为新的键腾出空间。

  4. 有序集合:Redis提供了有序集合(Sorted Set)数据类型,它是通过双端链表和散列表实现的。有序集合可以根据元素的分值进行排序,并支持按照分值范围获取元素。

总而言之,双端链表在Redis中的应用场景主要包括列表结构、发布与订阅、LRU缓存策略和有序集合。

四、双端链表在Redis中如何实现数据的插入和删除操作?

在Redis中,双端链表是通过使用list数据结构来实现的。Redis中的list数据结构使用双端链表来存储数据,并提供了一系列的命令来进行插入和删除操作。

下面是在Redis中实现双端链表数据插入和删除操作的一些常用命令:

  1. 插入操作:
  • LPUSH key value1 [value2 ...]:将一个或多个值插入到列表的头部。
  • RPUSH key value1 [value2 ...]:将一个或多个值插入到列表的尾部。
  • LINSERT key BEFORE|AFTER pivot value:在列表中指定元素的前面或后面插入一个新元素。
  1. 删除操作:
  • LPOP key:移除并返回列表头部的元素。
  • RPOP key:移除并返回列表尾部的元素。
  • LREM key count value:从列表中删除指定数量的元素。

需要注意的是,在双端链表中,插入和删除操作的时间复杂度都是O(1),即常数时间。

另外,Redis还提供了其他一些用于操作双端链表的命令,比如获取列表长度、获取指定范围内的元素等。

五、Redis双端链表与传统的单向链表有什么区别?

Redis双端链表和传统的单向链表有以下区别:

  1. 方向:传统的单向链表是单向的,每个节点只能指向下一个节点,而Redis的双端链表是双向的,每个节点既可以指向下一个节点,也可以指向前一个节点。

  2. 遍历:由于双端链表具有双向性,可以从头部或尾部开始遍历链表。传统的单向链表只能从头部开始遍历,如果需要从尾部开始遍历,则需要遍历整个链表到达尾部。

  3. 节点:双端链表的每个节点包含指向前一个节点和后一个节点的指针,而传统的单向链表的每个节点只包含指向下一个节点的指针。

  4. 操作:双端链表支持在链表的头部和尾部进行添加、删除节点的操作,并且这些操作的时间复杂度都是O(1)。传统的单向链表只能在头部进行添加和删除操作,如果需要在尾部进行添加和删除,则需要遍历整个链表到达尾部。

总的来说,Redis的双端链表在遍历和操作方面更加灵活和高效,特别适合用于实现队列和栈等数据结构。

六、Redis双端链表在高并发情况下可能会出现什么问题?如何解决这些问题?

在高并发情况下,Redis双端链表可能会出现以下问题:

  1. 竞争条件(Race Condition):多个线程同时修改链表结构,可能导致数据不一致或者链表的结构被破坏。

  2. 内存泄漏:在高并发情况下,如果有大量的插入和删除操作,可能会导致链表内存无法被及时释放,从而造成内存泄漏。

  3. 阻塞操作:当链表较长并且数据量较大时,插入和删除操作会变得较慢,从而阻塞其他并发操作。

为了解决这些问题,可以采取以下措施:

  1. 使用锁(Locking):通过在关键代码段使用适当的锁机制,可以保证每次只能有一个线程对链表进行修改,从而避免竞争条件。

  2. 使用乐观锁(Optimistic Locking):通过在链表节点中添加版本号(version)字段,并在修改节点时对版本号进行比较和更新,可以避免多个线程同时修改节点值和结构的问题。

  3. 分段锁(Segment Locking):将链表分成多个段,每个段都有一个锁。这样,在进行插入和删除操作时,只需要锁定对应的段,而不是整个链表,从而减少并发操作的阻塞。

  4. 使用更高效的数据结构:根据实际业务需求,考虑使用其他更高效的数据结构,如跳表(Skip List)或红黑树(Red-Black Tree),来替代双端链表。这些数据结构在并发情况下具有更好的性能和扩展性。

需要根据具体情况选择适当的解决方法,以保证在高并发情况下,Redis双端链表能够正常运行并保证数据一致性。

七、Redis双端链表如何处理内存分配和回收的问题?

Redis双端链表的内存分配和回收问题是通过Redis的内存分配器来处理的。Redis使用自己实现的内存分配器,默认是jemalloc,也可以配置为使用libc分配器或者其他内存分配器。

在双端链表的节点被创建时,Redis会使用内存分配器动态分配内存来存储节点的数据和指针等信息。当节点被删除或者不再使用时,Redis会通过内存分配器释放该节点所占用的内存。

Redis的内存分配器会维护一个内存池,用于存储分配出去的内存块。当需要分配内存时,内存分配器会从内存池中分配一块足够大小的内存,并将这块内存标记为已分配。当需要释放内存时,内存分配器会将已分配的内存块标记为可用,并将其返回给内存池,以供后续的内存分配。

Redis的内存分配器还会进行内存回收的优化,例如通过合并相邻的空闲内存块来减少内存碎片的产生。此外,Redis还会定期进行内存回收操作,将不再使用的内存块返回给操作系统,以便回收更多的内存空间。

总之,Redis双端链表的内存分配和回收问题主要是通过Redis的内存分配器来处理的,它负责分配和释放节点的内存,并进行内存回收的优化操作。

八、Redis是否支持链表的排序操作?如何实现链表的排序?

是的,Redis支持链表的排序操作。可以通过以下步骤来实现链表的排序:

  1. 使用RPUSH命令将元素依次插入到链表中。
  2. 使用LTRIM命令将链表裁剪为需要排序的部分。
  3. 使用SORT命令对链表进行排序。

下面是一个示例:

RPUSH mylist 5
RPUSH mylist 2
RPUSH mylist 10
RPUSH mylist 3
RPUSH mylist 8
LTRIM mylist 0 -1
SORT mylist ASC

执行完上述命令后,链表mylist中的元素将按升序排序。

九、Redis双端链表如何处理数据的重复问题?

Redis双端链表中并没有特别处理数据的重复问题。双端链表是一种有序的数据结构,可以存储重复的数据。如果需要处理数据的重复问题,可以在使用双端链表的时候,在外部进行处理,例如在插入前先检查数据是否已存在,或者在查询时排除重复的数据。

十、Redis双端链表在持久化时如何处理?

Redis中的双端链表在持久化时会被转化为一种特殊的序列化格式,然后存储在磁盘上。在加载数据时,Redis会重新构建双端链表。

具体来说,Redis会将双端链表中的每个节点都转化为一个字节序列。这个字节序列包括节点的值、前驱节点和后继节点的引用。这样,当需要恢复双端链表时,只需要按照序列化格式逐个节点进行解析,并重新构建双端链表的结构即可。

在持久化过程中,Redis会通过将链表的序列化格式写入到磁盘上的持久化文件中来保存链表的数据。而在加载数据时,Redis会从持久化文件中读取数据,并解析成链表的形式,以便于后续的操作。

需要注意的是,持久化过程中的链表数据可能会存在一些延迟,因为Redis使用的是异步的持久化机制。这意味着,在持久化数据时,可能会有一小段时间的数据丢失风险。但是大多数情况下,数据的丢失概率非常低。为了确保数据的可靠性,可以通过设置Redis的持久化策略,来控制持久化的频率和方式。

十一、小结

• Redis 实现了自己的双端链表结构。

• 双端链表主要有两个作用:

        – 作为 Redis 列表类型的底层实现之一;

        – 作为通用数据结构,被其他功能模块所使用;

• 双端链表及其节点的性能特性如下:

        –  节点带有前驱和后继指针,访问前驱节点和后继节点的复杂度为 O(1) ,并且对链表 的迭代可以在从表头到表尾和从表尾到表头两个方向进行;

        –  链表带有指向表头和表尾的指针,因此对表头和表尾进行处理的复杂度为 O(1) ;

        –  链表带有记录节点数量的属性,所以可以在 O(1) 复杂度内返回链表的节点数量(长

度);

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

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

相关文章

22. 常用shell之 chown - 更改文件所有者 的用法和衍生用法

chown 是 Unix 和类 Unix 系统(如 Linux 和 macOS)中用于更改文件或目录的所有者和所属组的命令。这个命令对于系统管理和文件权限管理至关重要。下面详细介绍 chown 的基本用法和一些衍生用法。 基本用法 chown 命令的基本语法如下: chow…

亚信科技AntDB数据库——深入了解AntDB-M元数据锁的实现(二)

5.5 防止低优先级锁饥饿 AntDB-M按照优先级将锁又分了两类,用于解决低优先级锁饥饿问题。 ●独占型(hog): X, SNRW, SNW; 具有较强的不兼容性,优先级高,容易霸占锁,造成其他低优先级锁一直处于等待状态。 ●暗弱型(piglet): SW; …

windows MinGW C语言编译器安装及环境变量配置教程

windows MinGW C语言编译器安装配置环境变量 MinGW安装 MinGW被称为Windows版的GCC,安装包下载地址: 提示:该安装包下载完之后,相当于安装好了MinGW,之后即可配置环境变量!所以,可以先新建好…

docker-compose elk部署elk 单节点版本

elk集群 docker-compose单节点运行版 机器分配 192.168.77.136 docker-compose 192.168.77.137 log-test cron 安装docker、docker-compose centos yum -y install docker-ce docker-composedebian/ubuntu apt -y install docker-ce docker-compose编写docker-compose.yaml…

算法训练day32|贪心算法part02

122.买卖股票的最佳时机 II 局部最优&#xff1a;记录每天的利润&#xff0c;只需要累加利润为正的天数 // 贪心思路 class Solution {public int maxProfit(int[] prices) {int result 0;for (int i 1; i < prices.length; i) {result Math.max(prices[i] - prices[i …

将数组中的数逆序存放

本题要求编写程序&#xff0c;将给定的n个整数存入数组中&#xff0c;将数组中的这n个数逆序存放&#xff0c;再按顺序输出数组中的元素。 输入格式: 输入在第一行中给出一个正整数n&#xff08;1≤n≤10&#xff09;。第二行输入n个整数&#xff0c;用空格分开。 输出格式:…

[Android] Binder all-in-all

前言&#xff1a; Binder 是一种 IPC 机制&#xff0c;使用共享内存实现进程间通讯&#xff0c;既可以传递消息&#xff0c;也可以传递创建在共享内存中的对象&#xff0c;而Binder本身就是用共享内存实现的&#xff0c;因此遵循Binder写法的类是可以实例化后在进程间传递的。…

加速数据采集:用OkHttp和Kotlin构建Amazon图片爬虫

引言 曾想过轻松获取亚马逊上的商品图片用于项目或研究吗&#xff1f;是否曾面对网络速度慢或被网站反爬虫机制拦截而无法完成数据采集任务&#xff1f;如果是&#xff0c;那么本文将为您介绍如何用OkHttp和Kotlin构建一个高效的Amazon图片爬虫解决方案。 背景介绍 亚马逊&a…

C# 避免定时器重入的4种方法

System.Timers.Timer执行方法的时候&#xff0c;会开一个线程去执行&#xff0c;如果使用锁&#xff08;方法3&#xff09;避免重入&#xff0c;可能会有多个线程等在那里执行。 按照单片机的写法要推荐方法1 但是C#提供了方便的定时器Stop方法&#xff0c;所以可用方法2&…

【MySQL】MySQL库的增删查改

文章目录 1.库的操作1.1创建数据库1.2创建数据库案例 2.字符集和校验规则2.1查看系统默认字符集以及校验规则2.2查看数据库支持的字符集2.3查看数据库支持的字符集校验规则2.4校验规则对数据库的影响 3.操纵数据库3.1查看数据库3.2显示创建语句3.3修改数据库3.4数据库删除3.5备…

KUKA机器人如何在程序中编辑等待时间?

KUKA机器人如何在程序中编辑等待时间&#xff1f; 如下图所示&#xff0c;如何实现在P1点和P2点之间等待设定的时间&#xff1f; 如下图所示&#xff0c;可以直接输入wait sec 2&#xff08;等待2秒&#xff09;&#xff0c; 如下图所示&#xff0c;再次选中该程序后&#…

python学习1

大家好&#xff0c;这里是七七&#xff0c;今天开始又新开一个专栏&#xff0c;Python学习。这次思考了些许&#xff0c;准备用例子来学习&#xff0c;而不是只通过一大堆道理和书本来学习了。啊对&#xff0c;这次是从0开始学习&#xff0c;因此大佬不用看本文了&#xff0c;小…

linux sed批量修改替换文件中的内容/sed特殊字符

sed系列文章 linux常用命令(9)&#xff1a;sed命令(编辑/替换/删除文本)linux sed命令删除一行/多行_sed删除第一行/linux删除文件某一行linux sed批量修改替换文件中的内容/sed特殊字符 文章目录 sed系列文章一、sed替换文本语法1.1、基础语法1.2、高阶语法 二、实战/实例2.1…

k8s常用命令及示例(三):apply 、edit、delete

k8s常用命令及示例(三)&#xff1a;apply 、edit、delete 1. kubectl apply -f 命令&#xff1a;从yaml文件中创建资源对象。 -f 参数为强制执行。kubectl apply和kubectl create的区别如下&#xff1a;kubectl create 和 kubectl apply 是 Kubernetes 中两个常用的命令&…

Springboot管理系统数据权限过滤(二)——SQL拦截器

上一节Springboot管理系统数据权限过滤——ruoyi实现方案对数据权限实现方案有了认识&#xff0c;本文将进一步优化权限过滤方案&#xff0c;实现对业务代码零入侵。 回顾上一章中权限方案&#xff1a; 主要是通过注解拦截&#xff0c;拼接好权限脚本后&#xff0c;放到对象变…

c题目17:写一个swap函数,可以交换2个整数变量的值。(分别用普通方式和指针方式实现,对比结果)

每日小语 我坐着&#xff0c;观望世界上所有的忧患&#xff0c;所有的压迫和耻辱看着&#xff0c;听着&#xff0c;一声不响。——惠特曼 自己思考 最近这段时间新的感悟似乎也没有&#xff0c;但我发现我和别人的思想越来越不同&#xff0c;只能跟极少数人产生共鸣&#xff0…

DevOps 和人工智能 – 天作之合

如今&#xff0c;人工智能和机器学习无处不在&#xff0c;所以它们开始在 DevOps 领域崭露头角也毫不令人意外。人工智能和机器学习正在通过自动化任务改变 DevOps&#xff0c;并使各企业的软件开发生命周期更高效、更深刻和更安全。我们在 DevOps 趋势中简要讨论过这一问题&am…

使用shell脚本给日志文件瘦身

一、前言 后台系统运行久了&#xff0c;日志文件的体积日渐增多&#xff0c;除了使用常用的日志框架如logback对日志进行按天打印、按大小分割等方式外&#xff0c;还可以使用shell命令来对大日志进行瘦身。 本篇使用sed指令来对文件进行操作&#xff0c;具体操作如下&#xf…

实现进程间的通信

本例程是开发一款能实现进程通信的DLL。本例程以Visual Studio 2015为例。在Visual Studio 2013&#xff0c;Visual Studio 2017都是可以。 第一步&#xff1a;在Visual Studio 2015中&#xff0c;创建DLL工程。如何创建DL&#xff0c;在这里就不作具体说明了。百度都有许多创建…

国际语音群呼系统有哪些应用场景?

国际语音群呼可应用于广告营销、消息通知、客情维护、金融催收等场景&#xff0c;助力出海企业产品营销和品牌推广。 广告营销 出海企业可以通过国际语音群呼系统&#xff0c;向目标市场的潜在客户进行广告宣传。例如&#xff0c;企业可以在系统中录制有关产品的宣传语&#…