四、Redis的对象类型与内存编码(重点)

        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的跳跃表实现由zskiplistzskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点。

        只有同时满足下面两个条件时,才会使用压缩列表:有序集合中元素数量小于128个;有序集合中所有成员长度都不足64字节。如果有一个条件不满足,则使用跳跃表;且编码只可能由压缩列表转化为跳跃表,反方向则不可能。

        下图展示了有序集合编码转换的特点:

78befe288ef2d7fe10ac6de5113305cc.png

(3)跳跃表

        图示

        普通单向链表图示:

        跳跃表图示:

3ebf8d863a5efd21b84ab02ae1c6e25f.png 

        跳跃表就是特殊的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的概率为3bc587913da90089fc23a52d8c3ad940.png,这个值的分子是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;

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值