Redis底层数据结构之List

1. 概述

Redis的列表类似于Java语言当中的LinkedList,但是还是存在着很大的区别的。

Redis3.2版本的前,使用两种数据结构作为底层实现:

  • 压缩列表zipList
  • 双向链表LinkedList

双向链表占用的内存比压缩链表要多,所以当创建新的列表键的时候,会优先考虑使用压缩列表,并且在有需要的时候,会转换成双向链表。

Redis3.2版本开始,Redis修改了list的底层实现,将压缩列表和双向链表结合,称之为quickList。

下面就先从3.2版本的底层结构开始说明。

2. 压缩列表zipList

压缩列表设计的初衷就是为了节约内存

它是由一系列特殊编码的内存块构成的,使用一块连续的内存空间存储。每个元素长度不同,采用的是变长编码。

如何起到节约内存的呢?

假设列表中的元素内容都很小,但是如果是双向列表的话就需要维护头尾两个指针,这是很浪费空间的。所以zipList在结构上可以得到上一个结点的长度和当前结点的长度。那么通过上一个结点的长度,就可以将指针定位到上一个元素起始的位置,而通过当前结点的长度,就可以将指针定位到下一个元素的起始位置。

zipList的内存内存结构如下图所示:

在这里插入图片描述

包括了头部信息和结点列表信息。

  • zlbytes: 表示整个zipList占用的内存字节数,对zipList进行内存重分配,或者计算末端时使用。
  • zltail: 表示到达ziplist表尾结点的偏移量。通过这个偏移量,可以直接定位到表位元素。
  • zllen:表示zipList中结点的数量。当这个值小于65535时,这个值就是zipList中结点的数量,当等于65535的时候,需要遍历整个zipList才能计算出结点的数量。
  • zlend:255的二进制值,用于表示zipList的末端

每个entry表示结点元素信息,采用的是变长编码。结点entry的结构如下:

typedef struct zlentry {    // 压缩列表节点
    unsigned int prevrawlensize, prevrawlen;    // prevrawlen是前一个节点的长度,prevrawlensize是指prevrawlen的大小,有1字节和5字节两种
    unsigned int lensize, len;  // len为当前节点长度 lensize为编码len所需的字节大小
    unsigned int headersize;    // 当前节点的header大小
    unsigned char encoding; // 节点的数据类型
    unsigned char *p;   // 指向节点的指针
} zlentry;

就和上面说的一样,每个结点可以得到前一个结点的长度 和当前结点的长度。

那么是如何变长编码的呢?

注意这里有一个prevrawlensize属性,它记录的是prevrawlen的大小,分成了两种

  • 若前一个结点的长度小于254字节,那么则使用1字节来存储prevrawlen;
  • 如果前一个结点的长度大于等于254字节,那么将第一个字节设置为254,然后接下来4个字节保存实际的长度。

上面的结构体的内容非常的多,上面的方式只是为了描述一个结点的设计需要考虑的东西,而实际上的entry结点的属性比上面要少一些,如下:前一个结点的长度就通过prevlen记录,而当前结点的长度就根据这3个属性的长度推出。

struct entry {
    int<var> prevlen; # 前一个 entry 的字节长度
    int<var> encoding; # 元素类型编码
    optional byte[] content; # 元素内容
}

3.ziplist连锁更新问题

每个zlentry结点都存储着前一个结点的所占的字节数,而这个数值是采用变长编码的。假设存在一个压缩列表,其中包含了e1,e2,e3…,e1结点的大小小于254,则e2中采用的是第一种的编码方式,也就是prevlen为1字节,若在e1和e2之间插入一个新的结点,这个新的结点的大小超过了254.那么此时e2中记录前一个结点的编码方式就需要修改,会多出4个字节。那么e2的整体长度就发生了变化,就会引起e3.prevlen发生改变,以此类推,当存在大量的结点接近254的时候,就会发生严重的连锁更新问题。

4.双向链表LinkedList

当链表中entry结点的数量超过512个、或单个value 长度超过64字节,底层就会转化成linkedlist编码。linkedlist是标准的双向链表,Node节点包含prev和next指针,可以进行双向遍历。还保存了 head 和 tail 两个指针,因此,对链表的表头和表尾进行插入的复杂度都为 (1) —— 这是高效实现 LPUSH 、 RPOP、 RPOPLPUSH 等命令的关键。linkedlist结构比较简单。

5.quickList

3.2版本开始采用quickList作为list的底层实现,结合了ziplist和LinkedList的优点。

quickList是一个zipList组成的双向链表。每个结点使用zipList来保存数据。本质上说quickList就是由一个一个小的zipList串起来的链表。如下图所示:

在这里插入图片描述

每个quickListNode包含了prev和next指针,分别指向前一个和下一个的qucikListNode。

quickListNode中又包含了zipList。

6.List的相关指令

介绍常用的一些指令

6.1push
  • lpush

    使用方法:lpush key value [value…]

    将一个或者多个value插入到列表key的表头

    当key不是列表类型或者key不存在 那么会返回一个错误在这里插入图片描述

  • Lpushx

    使用方法:lpushx key value [value…]

    和上面指令的区别在于,仅当key存在并且是一个列表的时候,才会执行,将value 插入列表的表头在这里插入图片描述

  • rpush/rpushx

    和上面的区别在于是在列表的表尾插入

6.2pop
  • lpop

    使用方法:lpop key

    移除并返回列表的表头元素,key不存在的时候,返回nil在这里插入图片描述

  • rpop

    使用方法: rpop key

    和上面的区别在于返回列表的表尾元素

6.3 rpoplpush

使用方法:rpoplpush source destination

原子操作,将列表source中的表尾元素弹出,返回给客户端,并将该元素插入到destination的表头

若source不存在则返回nil

在这里插入图片描述

6.4 lrem

使用方法:lrem key count value

根据count的值移除列表key中参数与value相等的元素

  • 当count > 0:从表头开始向后,移除value相等的元素,数量为count
  • 当count < 0:从表尾开始向前,移除value相等的元素,数量为count的绝对值
  • 当count = 0:移除列表中所有与value相等的元素。

返回被移除元素的数量。在这里插入图片描述

6.5 llen

使用方法:llen key

返回列表key的长度,key不存在返回0

6.6 lindex

使用方法:lindex key index

返回列表key中下标为index的元素

0表示第一个元素

index可以是负数,-1表示最后一个元素,-2表示倒数第二个元素,以此类推。

若key不是列表返回错误。

在这里插入图片描述

6.7 linsert

使用方法:linsert key before|after pivot value

将值value插入在列表key中 pivot的之前或之后的位置。

  • 若pivot不存在于列表中,不执行任何操作。返回-1
  • 若key不存在,不执行任何操作。返回0
  • 若key不是列表,返回一个错误。

在这里插入图片描述

6.8 lset

使用方法:lset key index value

将列表key中下标为index的值设置为value

  • 若index参数超过列表返回,或对一个空列表进行lset,返回一个错误。
  • 操作成功返回ok

在这里插入图片描述

6.9 lrange

使用方法:lrange key start stop

返回列表当中 start到stop的元素,闭区间

  • 若start下标比列表最大下标大,返回一个空列表
  • 若stop下标比列表最大下标大,会将stop设置为最大下标的值

在这里插入图片描述

6.10 ltrim

使用方法:ltrim key start stop

对列表key 进行修剪,仅保留start到stop返回的数据,闭区间,其他删除。

  • 若key不是列表返回错误
  • 若start>stop或start下标比列表最大下标大,则返回一个空列表,表示全部清空
  • 若stop下标比列表最大下标大,会自动将stop设置为最大下标的值

在这里插入图片描述

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值