文章目录
一、跳跃表
跳跃表(skiplist)是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的,在大部分情况下,跳跃表的效率可以和平衡树媲美,而且跳跃表的实现比平衡树更简单
跳跃表的大致实现如图:
1. Redis中的跳跃表
Redis只在两个地方用到跳跃表,一个是实现有序集合的键,一个是集群节点中用作内部数据结构
Redis中的跳跃表主要由节点zskiplistNode和跳表zskiplist来实现
1.1 zskiplistNode 跳跃表节点
跳跃表节点的实现由 redis.h / zskiplistNode 结构定义:
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
1.1.1 层
-
跳跃表的结点的level 数组可以包含多个元素,每个元素包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度
-
每次创建一个新跳跃表节点的时候,程序都根据幂次定律(越大的数出现的概率越低)随机生成一个1 ~ 32 的值作为level数组的大小,这个值就是层的高度
-
因为层的字段只有forward 前进指针,所以是一个单链表,而所有zskiplistNode结构中有backward字段,所以是一个双链表,就相当于,跳跃表结构最下面一层是双链表,上面的层数则为双链表,如图
-
例如查找45号元素,则先从最上层的头结点开始查找,如果下一个结点是比自己大或者是NULL之后则向下一层查找
-
再下一层也如同上一步查找,直到找到最下一层,再通过范围内遍历节点找到45
-
层的跨度(level[i].span 属性) 用于记录两个节点之间的距离。主要用于计算rank,
rank是排位,头节点开始到目标节点的跨度,由沿途的span相加获得
1.1.2 分值和成员
- 节点的分值(score属性): 是一个double类型的浮点数,跳跃表中所有节点都按分值从小到大排序
- 节点的成员对象(obj属性): 是一个指针,指向一个字符串SDS对象
- score和obj共同来决定一个元素在跳表中的顺序。score不同则根据score进行排序,score相同则根据obj来进行排序
跳表中score是可以相同的,而obj是肯定不同的
1.2 zskiplist 跳跃表
仅靠多个跳跃表节点就可以组成一个跳跃表
但是通过一个zskiplist 结构可以对整个表更方便的做一些处理
zskiplist 结构如下
typedef struct zskiplist {
// 头尾指针,用于保存头结点和尾节点
struct zskiplistNode *header, *tail;
// 跳跃表的长度,即除头结点以外的节点数
unsigned long length;
// 最大层数,保存了节点中拥有的最大层数(不包括头结点)
int level;
} zskiplist;
二、整数集合
整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现,例如
1. 整数集合的实现
整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素且按照从小到大的顺序排列
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
- contents数组是整数集合的底层实现,每个项在数组中按值的大小从小到大有序排列,并且不包含重复项。另外,虽然intset结构将contents属性声明为int8_t,但实际上数组类型由encoding决定
- 如果encoding属性的值为INTSET_ENC_INT16,那么contents就是一个int16_t类型的数组,数组里的每个项都是一个int16_t类型的整数值
- 如果encoding属性的值为INTSET_ENC_INT32,那么contents就是一个int32_t类型的数组,数组里的每个项都是一个int32_t类型的整数值
- 以此类推
- length 属性记录了整数集合包含的元素数量,也就是数组长度
2. 升级
每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级(upgrade),然后才能将新元素添加到整数集合里面
升级并且添加新元素步骤
- 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
- 将底层数组(contents[])现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变
- 将新元素添加到底层数组里面,因为该数的类型长度是最长的所以要么放在第一个,要么放在最后一个
升级的好处
- 提升灵活性:C语言是静态类型语言,为了避免类型错误,一般不会将两种不同的类型的值放在一个数据结构中,通过升级可以自动适应新元素,就可以随意的将各种类型整数加入其中
- 节约内存:会用尽量小的空间保存数据,节约内存,例如如果所有类型都是int16_t ,那就不会采用更大的类型
整数集合不支持降级的操作,一旦对数组进行了升级,编码就会一直保持升级后的状态
三、压缩列表
压缩列表(ziplist)是列表键(list)和哈希键(hash)的底层实现之一
- 当一个列表键(list)只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现(根据书本上的例子使用的好像是快表)
- 当一个哈希键只包含少量键值对,比且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希键的底层实现
1. 压缩列表的构成
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。具体组成如下
属性 | 类型 | 长度 | 用途 |
---|---|---|---|
zlbytes | uint32_t | 4字节 | 记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配或者计算zlend的位置时使用 |
zltail | uint32_t | 4字节 | 记录压缩列表表尾节点举例压缩列表的起始地址有多少字节,如果我们有一个指向压缩列表起始地址的指针p,通过p+zltail就能直接访问压缩列表的最后一个节点 |
zllen | uint16_t | 2字节 | 压缩列表中的节点数,当这个值大于UINT16_MAX 之后,就得遍历压缩列表才能得到节点数 |
entryX | 列表节点 | 不定 | 压缩列表包含的各个节点 |
zlend | uint8_t | 1字节 | 标记压缩列表的末端(我认为用于指针碰撞分配内存) |
2. 压缩列表节点的构成
每个压缩列表节点都由previous_entry_length、encoding、content三个部分组成
previous_entry_length
- 节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度,其值长度为1个字节或者5个字节
- 如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节
- 如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节
- 因为节点的previous_entry_length属性记录了前一个节点的长度,所以程序可以通过指针运算,计算出前一个结点的起始地址。
- 因为内存是连续的所以用过这种方法减小前驱指针的内存
encoding
- 节点的encoding属性记录了节点的content属性所保存数据的类型以及长度
content
- 节点的content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定
3. 连锁更新
每个节点的previous_entry_length属性都记录了前一个节点的长度,如果,有多个连续的长度介于250~253字节之间的节点,那么在第一个节点之前插入了一条大于254字节的节点,这时候下一个结点previous_entry_length的长度就得变为五个字节,则他的总长度就变为了255,则下一个节点也需要更新,导致一连串连锁更新
删除节点同理,也可能造成这种反应
因为连锁更新在最坏情况下需要对压缩列表执行N次空间重分配操作,而每次分配的最坏复杂度为O(N),所以连锁更新最坏复杂度为O(N²)
但是这种情况还是小概率事件,依然可以放心使用压缩列表
四、快表
1. 快表的结构
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节点
quicklist结构
typedef struct quicklist {
//指向头部(最左边)quicklist节点的指针
quicklistNode *head;
//指向尾部(最右边)quicklist节点的指针
quicklistNode *tail;
//ziplist中的entry节点计数器
unsigned long count; /* total count of all entries in all ziplists */
//quicklist的quicklistNode节点计数器
unsigned int len; /* number of quicklistNodes */
//保存ziplist的大小,配置文件设定,占16bits
int fill : 16; /* fill factor for individual nodes */
//保存压缩程度值,配置文件设定,占16bits,0表示不压缩
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
quicklist节点结构
typedef struct quicklistNode {
struct quicklistNode *prev; //前驱节点指针
struct quicklistNode *next; //后继节点指针
//不设置压缩数据参数recompress时指向一个ziplist结构
//设置压缩数据参数recompress指向quicklistLZF结构
unsigned char *zl;
//压缩列表ziplist的总长度
unsigned int sz; /* ziplist size in bytes */
//ziplist中包的节点数,占16 bits长度
unsigned int count : 16; /* count of items in ziplist */
//表示是否采用了LZF压缩算法压缩quicklist节点,1表示压缩过,2表示没压缩,占2 bits长度
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
//表示一个quicklistNode节点是否采用ziplist结构保存数据,2表示压缩了,1表示没压缩,默认是2,占2bits长度
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
//标记quicklist节点的ziplist之前是否被解压缩过,占1bit长度
//如果recompress为1,则等待被再次压缩
unsigned int recompress : 1; /* was this node previous compressed? */
//测试时使用
unsigned int attempted_compress : 1; /* node can't compress; too small */
//额外扩展位,占10bits长度
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
2. 快表的常用操作
① 插入
quicklist可以选择在头部或者尾部进行插入(quicklistPushHead和quicklistPushTail),而不管是在头部还是尾部插入数据,都包含两种情况:
- 如果头节点(或尾节点)上ziplist大小没有超过限制(即_quicklistNodeAllowInsert返回1),那么新数据被直接插入到ziplist中(调用ziplistPush)。
- 如果头节点(或尾节点)上ziplist太大了,那么新创建一个quicklistNode节点(对应地也会新创建一个ziplist),然后把这个新创建的节点插入到quicklist双向链表中。
也可以从任意指定的位置插入。quicklistInsertAfter和quicklistInsertBefore就是分别在指定位置后面和前面插入数据项。这种在任意指定位置插入数据的操作,要比在头部和尾部的进行插入要复杂一些。
- 当插入位置所在的ziplist大小没有超过限制时,直接插入到ziplist中就好了;
- 当插入位置所在的ziplist大小超过了限制,但插入的位置位于ziplist两端,并且相邻的quicklist链表节点的ziplist大小没有超过限制,那么就转而插入到相邻的那个quicklist链表节点的ziplist中;
- 当插入位置所在的ziplist大小超过了限制,但插入的位置位于ziplist两端,并且相邻的quicklist链表节点的ziplist大小也超过限制,这时需要新创建一个quicklist链表节点插入。
- 对于插入位置所在的ziplist大小超过了限制的其它情况(主要对应于在ziplist中间插入数据的情况),则需要把当前ziplist分裂为两个节点,然后再其中一个节点上插入数据
② 查找
quicklist查找元素主要是针对index,即通过元素在链表中的下标查找对应元素。基本思路是,首先找到index对应的数据所在的quicklistNode节点,之后调用ziplist的接口函数ziplistGet得到index对应的数据。简而言之就是:定位quicklistNode,再在quicklistNode中的ziplist中寻找目标节点
③ 删除
区间元素删除的函数是 quicklistDelRange
quicklist 在区间删除时,会先找到 start 所在的 quicklistNode,计算删除的元素是否小于要删除的 count,如果不满足删除的个数,则会移动至下一个 quicklistNode 继续删除,依次循环直到删除完成为止。
quicklistDelRange 函数的返回值为 int 类型,当返回 1 时表示成功的删除了指定区间的元素,返回 0 时表示没有删除任何元素。