Redis支持五种对象类型,而每种结构都有至少两种编码;
这样做的好处在于:
一方面接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响;
另一方面可以根据不同的应用场景切换内部编码,提高效率。
Redis各种对象类型支持的内部编码如下图所示(只列出重点的):
1.字符串(SDS)
(1)概况
字符串是最基础的类型,因为所有的键都是字符串类型,且字符串之外的其他几种复杂类型元素也是字符串
字符串长度不超过512MB。
(2)内部编码
字符串类型的内部编码有3种,他们应用场景如下:
- int:8个字节的长整型。字符串值是整型时,这个值使用long整型表示。
- embstr:<=44字节的字符串。embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。
- raw:大于44个字节的字符串
3.2之后 embstr和raw进行区分的长度,是44;是因为redisObject的长度是16字节,sds的长度是4+字符串长度;因此当字符串长度是44时,embstr的长度正好是16+4+44 =64,jemalloc正好
可以分配64字节的内存单元。
2.列表
(1)概况
列表(list)用来存储多个有序的字符串,每个字符串称为元素;
一个列表可以存储2^32-1个元素。
Redis中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、栈等。
linkedList
(2)内部编码
Redis3.0之前列表的内部编码可以是压缩列表(ziplist)或双端链表(linkedlist)。选择的折中方案是两种数据类型的转换,但是在3.2版本之后 因为转换也是个费时且复杂的操作,引入了一种新的数据格式,结合了双向列表linkedlist和ziplist的特点,称之为quicklist。所有的节点都用quicklist存储,省去了到临界条件是的格式转换。
(3)压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表只包含少量列表项时,并且每个列表项时小整数值或短字符串,那么Redis会使用压缩列表来做该列表的底层实现。
压缩列表(ziplist)是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值,放到一个连续内存区。
- previous_entry_ength: 记录压缩列表前一个字节的长度。
- encoding:节点的encoding保存的是节点的content的内容类型
- content:content区域用于保存节点的内容,节点内容类型和长度由encoding决定。
(4)双向链表
双向链表(linkedlist):由一个list结构和多个listNode结构组成;
典型结构图如下:
通过图中可以看出,双端链表同时保存了表头指针和表尾指针,并且每个节点都有指向前和指向后的指针;链表中保存了列表的长度;dup、free和match为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。而链表中每个节点指向的是type为字符串的redisObject。
(5)快速列表
简单的说,我们仍旧可以将其看作一个双向列表,但是列表的每个节点都是一个ziplist,其实就是linkedlist和ziplist的结合,quicklist中每个节点ziplist都能存储多个数据元素。
Redis3.2开始,列表采用quicklist进行编码。
//32byte 的空间
typedef struct quicklist{
//指向quicklist的头部
quicklistNode *head;
//指向quicklist的尾部
quicklistNode *tail;
//列表所有数据项的个数总和
unsigned long count;
// quicklist节点的个数,即ziplist的个数
unsigned int len;
// ziplist大小限定,由list-max-ziplist-size给定
// 表示不用整个int存储fill,而是只用了其中的16位来存储
int fill : 16;
// 节点压缩深度设置,由list-compress-depth给定
unsigned int compress : 16;
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; // 指向上一个ziplist节点
struct quicklistNode *next; // 指向下一个ziplist节点
unsigned char *zl; // 数据指针,如果没有被压缩,就指向ziplist结构, 反之指向quicklistLZF结构
unsigned int sz; // 表示指向ziplist结构的总长度(内存占用长度)
unsigned int count : 16; // 表示ziplist中的数据项个数
unsigned int encoding : 2; // 编码方式,1--ziplist,2--quicklistLZF
unsigned int container : 2; // 预留字段,存放数据的方式,1--NONE,2-- ziplist
unsigned int recompress : 1; // 解压标记,当查看一个被压缩的数据时,需要暂时解 压,标记此参数为1,之后再重新进行压缩
unsigned int attempted_compress : 1; // 测试相关
unsigned int extra : 10; // 扩展字段,暂时没用
} quicklistNode;
3.哈希
(1)概况
哈希(作为一种数据结构),不仅是redis对外提供的5种对象类型的一种(与字符串、列表、集合、有序列结合并列),也是Redis作为Key-Value数据库所使用的数据结构。为了说明的方便,后面使用“内层的哈希”时,代表的是redis对外提供的5种对象类型的一种;使用“外层的哈希”代指Redis作为key-value数据库所使用的数据结构。
(2)内部编码
内层的哈希使用的内部编码可以是压缩列表(ziplist)和哈希表(hashtable)两种;redis的外层的哈希则只使用了hashtable。
压缩列表前面已介绍。与哈希表相比,压缩列表用于元素个数少、元素长度小的场景;其优势在于集中存储,节省时间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于哈希种元素数量较少,因此操作时间并没有明显劣势。
hashtable:一个hashtable由1个dict结构、2个dict结构、1个dictEntry指针数组(称为bucket)和多个dictEntry结构组成。
正常情况下(即hashtable没有进行rahash时)各部分关系如下图所示:
dict
一般来说,通过使用dicth和dictEntry结构,便可以实现普通哈希表的功能;但是Redis的实现种,在dictht结构的上层,还有一个dict结构。下面说明dict结构的定义及作用。
dict结构如下:
typedef struct dict{
dictType *type; // type里面主要记录了一系列的函数,可以说是规定了一系列的接口
void *privdata; // privdata保存了需要传递给那些类型特定函数的可选参数
//两张哈希表
dictht ht[2];//便于渐进式rehash
int trehashidx; //rehash 索引,并没有rehash时,值为 -1
//目前正在运行的安全迭代器的数量
int iterators;
} dict;
typedef struct dict{
dictType *type;
void *privdata;
dictht ht[2];
int trehashidx; //rehash int
iterators;
} dict;
其中,type属性和privdata属性是为了适应不同类型的键值对,用于创建多态字典。
ht属性和trehashidx属性则用于rehash,即当哈希表需要扩展或收缩时使用。ht是一个包含两个项的数组,每项都指向一个dictht结构,这也是Redis的hash会有一个dict、2个dictht结构的原因。通常情况下,所有的数据都是存放在dict的ht[0]中,ht[1]只在rehash的时候使用。dict进行rehash操作的时候,将ht[0]中的所有数据rehash到ht1[1]中。然后将ht[1]赋值给ht[0],并清空ht[1]。
因此,Redis中的哈希之所以在dicth和dictEntry结构之外还有一个dict结构,一方面是为了适应不同类型的键值对,另一方面是为了rehash。
于567 余568 浴
解放冲突 开放地址法
链地址法
dictht
dictht结构如下:
typedef struct dictht{
//哈希表数组,每个元素都是一条链表
dictEntry **table;
//哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
}dictht;
其中,各个属性的功能说明如下:
- table属性是一个指针,指向bucket;
- size属性记录了哈希表的大小,即bucket;
- used记录了已使用dictEntry的数量;
- sizemask属性的值总是为size-1,这个属性和哈希值一起决定一个键在table中存储的位置。
bucket
dictEntry结构用于保存键值对,结构定义如下:
// 键
typedef struct dictEntry{
void *key;
union{ //值v的类型可以是以下三种类型
void *val;
uint64_tu64;
int64_ts64;
}v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
其中,各个属性的功能如下:
- key:键值对中的键
- val:键值对中的值,使用union(即共用体)实现,存储的内容即可能是一个指向值的指针,也可能时64位整型,或无字符号64位整型;
- next:指向下一个dictEntry,用于解决哈希冲突问题
在64位系统中,一个dictEntry对象占24个字节(key/val/next各占8个字节)
(3)编码转换
如前所述,Redis中内层的哈希即可能使用哈希表,也可能使用压缩列表。
只有同时满足下面两个条件时,才会使用压缩列表:
- 哈希表中元素数量小于512个;
- 哈希表所有键值对的键和值字符串长度都小于64个字节。
下图展示了Redis内层的哈希编码转换的特点:
4.集合(整数集合和哈希表)
(1)概况
集合(set)与列表类似,都是用来保存多个字符串,但集合与列表有两点不同;集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。
一个集合最多可以存储2^31-1个元素;除了支持常规的增删改查,Redis还支持多个集合取交集、并集、差集。
(2)内部编码
集合的内部编码可以是整数集合(intset)或哈希表(hashtable)。
哈希表前面已经讲过,这里略过不提;需要注意的是,集合在使用哈希表时,值全部被置为null。
整数集合的结构定义如下:
typedef struct intset{
uint32_t encoding; // 编码方式
uint32_t length; // 集合包含的元素数量
int8_t contents[]; // 保存元素的数组
} intset;
其中,encoding代表contents中存储内容的类型,虽然contents(存储集合中的元素)是int8_t类型,但实际上其存储的值是int16_t、int32_t或int64_t,具体的类型便是由encoding决定的;length表示元素个数。
整数集合适用于集合所有元素都是整数且集合元素数量较小的时候,与哈希表相比,整数集合的优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于集合数量较少,因此操作的时间并没有明显劣势。
(3)编码转换
只有同时满足下面两个条件,集合才会使用整数集合:
- 集合中元素数量小于512个
- 集合中所有元素都是整数值。
如果有一个条件不满足,则使用哈希表;且编码只可能由整数集合转化为哈希表、反方向则不可能。
下图展示了集合编码转换的特点:
5.有序集合(压缩列表和跳跃表)
(1)概况
有序集合的内部编码可以是压缩列表(ziplist)或跳跃列表(skiplist)。ziplist在列表和哈希表中使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。
(2)内部编码
有序集合的内部编码可以是压缩列表(ziplist)或跳跃表(skiplist)。ziplist在列表和哈希中都有使用,前面已经讲过,这里略过不提。
跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此redis中选用跳跃表代替平衡树。跳跃表支持平均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作。
[Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点。
只有同时满足下面两个条件时,才会使用压缩列表:有序集合中元素数量小于128个;有序集合中所有成员长度都不足64字节。如果有一个条件不满足,则使用跳跃表;且编码只可能由压缩列表转化为跳跃表,反方向则不可能。
下图展示了有序集合编码转换的特点:
(3)跳跃表
图示
普通单向链表图示:
跳跃表图示:
跳跃表就是特殊的linkedlist
到19普通链表 3-6-7-9-12-17-19
跳跃表 21-9-21-17-21-19
插入
L1层
概率算法
在此还是以上图为例:跳跃表的初试状态如下图所示,表中没有一个元素:
如果我们要插入元素2,首先是在底部插入元素2,如下图:
然后我们抛硬币,结果是反面,那么我们要将2插入到L2层,如下图
继续抛硬币,结果是反面,那么元素2插入操作就停止了,插入后的表结构就是上如所示。接下来,我们插入元素33,跟元素2插入一样,现在L1层插入33,如下图:
然后抛硬币,结果是反面,那么元素33的插入操作就结束了,插入后的表结构就是上图所示。接下来,我们插入元素55,首先在L1插入55,插入后如下图:
然后抛硬币,结果是正面,那么L2层需要插入55,如下图:
继续抛硬币,结果又是正面,那么L3层需要插入55,如下图:
继续抛硬币,结果又是正面,那么要在L4插入55,结果如下图:
继续抛硬币,结果是反面,那么55的插入结束,表结构就如上图所示。
以此类推,我们插入剩余的元素。当然因为规模小,结果可能不是一个理想的跳跃表。但是如果元素个数N的规模很大,学过概率论的同学都知道,最终结构肯定非常接近理想跳跃表(隔一个一跳。)
优化后
随机数1-32
实际插入:
节点层数恰好等于1的概率为1-p(p为1/4)。3/4
每层概率(1/4)^n-1*(1-1/4)
那么节点层数恰好等于32的概率为
,这个值的分子是3,分母是(18,446,744,073,709,551,616),是千亿亿级的数字,比中彩票的概率还小。
删除
直接删除元素,然后调整一下删除元素的指针即可。跟普通的链表删除操作完全一样。
总结
①、搜索:从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下找,也就是和当前层的下一层的节点的下一个节点进行比较,以此类推,一直找到最底层的最后一个节点,如果找到则返回,反之则返回空。
②、插入:首先确定插入的层数,有一种方法是假设抛一枚硬币,如果是正面就累加,直到遇见反面为止,最后记录正面的次数作为插入的层数。当确定插入的层数k后,则需要将新元素插入到从底层到k层。
③、删除:在各个层中找到包含指定值的节点,然后将节点从链表中删除即可,如果删除以后只剩下头尾两个节点,则删除这一层。
最开始,使用抛硬币的方式现在第一层插入抛硬币正面就 2层继续抛硬币之道是反面就结束。
跳跃表的完整实现
typedef struct zskiplistNode {
//层
struct zskiplistLevel{
//前进指针 后边的节点
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
} zskiplistNode
--链表
typedef struct zskiplist{
//表头节点和表尾节点
structz skiplistNode *header, *tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}zskiplist;