草庐IT

Redis 数据结构-双向链表

涛姐涛哥 2023-04-17 原文

Redis 数据结构-双向链表

 

    最是人间留不住,朱颜辞镜花辞树。

 

1、简介

Redis 之所以快主要得益于它的数据结构、操作内存数据库、单线程和多路 I/O 复用模型,进一步窥探下它常见的五种基本数据的底层数据结构。

Redis 常见数据类型对应的的底层数据结构。

  • String:简单动态字符串。
  • List:双向链表、压缩列表。
  • Hash:压缩列表、哈希表。
  • Sorted Set:压缩列表、跳表。
  • Set:哈希表、整数数组。

2、双向链表

C 语言是没有内置链表这种结构,而当一个列表键包含较多的元素,或者列表中包含的元素都是比较长的字符串的时,Redis 就会使用链表作为 list 的底层实现,就自己实现了双向链表,相当于Java 语言中的LinkedList,但又不完全是。

单纯的链表优缺点:

  • 双向链表数据结构,支持前后顺序遍历。
  • 不需要连续的的内存空间,插入和删除的时间复杂度是 O(1) 级别的,效率较高。
  • 比起数组它的缺点就是查询较慢(时间复杂度O(n))。

常见使用场景

双向链表的特性经常被用于异步队列的使用。实际开发中将需要延后处理的任务结构体序列化成字符串,放入Redis 的队列中,另一个线程从这个列表中获取数据进行后续的业务逻辑。

链表节点结构

从redis/src/adlist.h 源码文件中查看链表节点结构设计。

1 typedef struct listNode {
2  struct listNode *prev; // 前置节点,如果是list的头结点,则prev指向NULL 
3  struct listNode *next;// 后置节点,如果是list尾部结点,则next指向NULL 
4  void *value; // 记录该节点的值,能够存放任何信息(也叫万能节点)
5 } listNode;

从listNode 结构中看到一个节点由头指针 prev 、尾指针 next  以及节点的值 value 组成,这种有前置节点和后置节点很明显就是一个双向链表。

链表结构

为了方便操作,Redis 在 listNode 链表节点结构体基础上又封装了 list 这个数据结构,而且封装之后,还提供了头节点、尾节点以及一些自定义的函数。链表结构如下:

 1 typedef struct list { 
 2   listNode *head; // 链表 头结点 指针 
 3   listNode *tail; // 链表 尾结点 指针
 4   unsigned long len; // 链表长度计数器 即 节点的个数
 5  
 6   // 三个函数指针
 7   void *(*dup)(void *ptr); // 复制函数 复制链表节点保存的值 
 8   void (*free)(void *ptr); // 释放函数 释放链表节点保存的值
 9   int (*match)(void *ptr, void *key); // 匹配函数 查找节点时使用 比较链表节点所保存的节点值和另一个输入的值是否相等 
10 } list;

list 结构为链表提供了链表头指针 head、链表尾节点 tail、链表节点数量 len、以及可以自定义实现的 dup、free、match 函数。

  • head:链表 头结点 指针,指向了双向链表的最开始的一个节点;
  • tail:链表 尾结点 指针,指向了双向链表的最后一个节点;
  • len:代表了双向链表节点的数量;
  • dup:复制函数,用于复制双向链表节点所保存的值;
  • free:释放函数,用于释放双向链表节点所保存的值;
  • match:匹配函数,用于对比双向链表节点所保存的值和另外一个的输入值是否相等。

相关命令

右进左出(队列)

队列在结构上是先进先出(FIFO)的数据结构(比如排队购票的顺序),常见场景如消息排队、异步处理等,用于确保元素的访问顺序。
lpush -> 从左边边添加元素

127.0.0.1:6379> lpush tjt_list 1 2 3

rpush -> 从右边添加元素

127.0.0.1:6379> rpush tjt_list 1 2 3

llen -> 获取列表的长度

127.0.0.1:6379> llen tjt_list

lpop -> 从左边弹出元素

127.0.0.1:6379> lpop tjt_list

右进右出(

栈在结构上是先进后出(FILO)的数据结构(比如弹夹压入子弹,子弹被射击出去的顺序就是栈),这种数据结构一般用来处理一些逆序输出的业务场景。

lpush -> 从左边边添加元素

127.0.0.1:6379> lpush tjt_list 1 2 3

rpush -> 从右边添加元素

127.0.0.1:6379> rpush tjt_list 1 2 3

rpop -> 从右边弹出元素

127.0.0.1:6379> rpop tjt_list

慢操作

由于列表(list)的链表数据结构,它的遍历是慢操作,所以涉及到遍历的性能将会随着遍历区间range 的增大而增大。在Redis 链表中,list 的索引运行为负数,-1代表倒数第一个,-2代表倒数第二个,其它同理。

lindex -> 遍历获取列表指定索引处的值(下方所有为 0)

127.0.0.1:6379> lindex tjt_list 0
"you"

lrange -> 获取从索引start 到stop 处的全部值

127.0.0.1:6379> lrange tjt_list 0 -1
1) "you"
2) "will"
3) "never"
4) "know"

ltrim -> 截取并保存索引start 到stop 处的全部值,其它将会被删除

127.0.0.1:6379> ltrim tjt_list 1 -1
OK
127.0.0.1:6379> lrange tjt_list 0 -1
1) "will"
2) "never"
3) "know"

非普通LinkedList

前面提到了Redis 数据类型List 对应的的底层数据结构有 双向链表 和 压缩列表,因为 Redis底层存储list(列表)不是一个简单的LinkedList,而是quicklist 快速列表。

为什么用quicklist 替代LinkedList

普通的LinkedList node节点元素,都会持有一个prev-> 执行前一个node 节点和next-> 指向后一个node 节点的指针(引用),这种结构虽然支持前后顺序遍历,但是也带来了不小的内存开销,如果node 节点仅仅是一个int 类型的值,那么引用的内存比例将会更大。所以Redis 底层对于list(列表)的存储,当元素个数少的时候,它会使用一块连续的内存空间来存储,这样可以减少每个元素增加prev 和next 指针带来的内存消耗,减少内存碎片化问题。

quicklist

quicklist 是多个ziplist (压缩列表)组成的双向列表。

ziplist

ziplist是一块连续的内存地址,他们之间无需持有prev和next指针,能通过地址顺序寻址访问。

3、链表小结

链表优势

  • listNode 链表节点的结构里带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表,对双向链表的访问以 NULL 结束;
  • list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头、表尾节点的时间复杂度都是 O(1);
  • list 结构因为提供了链表节点数量 len,通过 len 属性直接获取节点的数量,时间复杂度为 O(1) , 效率高;
  • listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值。

​链表缺陷

  • 链表每个节点之间的内存都是不连续的,无法很好利用 CPU 缓存。数组数据结构就能很好利用 CPU 缓存,因为数组的内存是连续的,可以充分利用 CPU 缓存来加速访问。
  • 保存一个链表节点的值每次都需要为一个链表节点结构头分配空间,内存开销较大。Redis 3.0 的 List 对象在数据量比较少的情况下,会采用 压缩列表 作为底层数据结构的实现,节省内存空间,降低保存链表节点的内存开销。

 

 

 
最是人间留不住
朱颜辞镜花辞树
 
 
 

 

 

有关Redis 数据结构-双向链表的更多相关文章

  1. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  4. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  5. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  6. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  7. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  8. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

  9. ruby-on-rails - 创建 ruby​​ 数据库时惰性符号绑定(bind)失败 - 2

    我正在尝试在Rails上安装ruby​​,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf

  10. STM32读取串口传感器数据(颗粒物传感器,主动上传) - 2

    文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,

随机推荐