二、Redis常用数据类型以及底层数据结构分析

目录

前言

正文

一、Redis几种数据类型的底层数据结构

二、压缩列表和跳表详解

压缩列表(ziplist):

跳表(skipList)

三、文中思考题

四、总结

五、参考文献


前言

       施主,既然来了,就静下心来,仔细阅读好好思考!
       老衲才疏学浅,如有错误,还请各位大侠在评论区不吝赐教。

正文

一、Redis几种数据类型的底层数据结构

       redis常用的数据类型分别是:字符串、列表、哈希字典表、集合、有序集合,当然还有几个高级的数据结构:geo、hyperloglog、bitmap,后续文章介绍。先讲常用的数据类型底层的数据结构是什么。

字符串(String):对应的数据结构就是字符串,这里不展开讲
redis对字符串类型的数据结构操作的基本命令:

  • set key value :设置指定key的值,如果存在key值,则相当于是修改操作;
  • get key :获取指定的key的值;
  • getset key value :将给定key的值设为value,并且返回key的久值(如果在这之前不存在key,则返回空nil);
  • mget key1 [key2...] :获取一个或多个指定的key的值,比如mget key1 key2 就是获取对应key1 key2的值;
  • mset key1 value [key2 value2....] :设置一个或多个key value的值。这种批量的获取和设置有利于减少应用服务器请求redis服务的次数,如果分单次请求就意味着每次都要进行应用服务请求redis服务的请求和响应时间
  • setex key seconds value :设置指定过期时间为second的key的值,时间单位为秒。psetex key milliseconds value命令就是以毫秒为单位;
  • setnx key value :设置指定的key的值,如果key值存在,则设置不成功返回值为0,如果key值不存在,则设置成功并且返回值为1。这个命令在实际的开发中多用于分布式锁中;
  • incr key :将存储数字的key的值增加一。如果key存储的值不是数值,执行该命令会报error错误,如果增加成功会返回增加后的值;
  • incrby key increment :将存储数字的key的值增加increment。和incr key的执行结果是等同的;
  • decr keydecrby key increment 就是和上面的增加是相反的操作。

列表(List):列表这种数据类型支持存储一组数据,底层两种数据结构实现方式:压缩列表、双向循环链表
使用压缩列表的情况:
       ① 列表中保存的单个数据的大小小于64字节;
       ② 列表中数据的个数少于512个。
也就是在列表中存储的数据量比较小的时候,采用的是压缩列表形式。

       为什么?文章后面第三节会跟大家一起分析,在这之前我们先了解redis各数据类型底层的数据结构,再详细了解对应的数据结构底层原理。

redis中对列表操作的常用命令:

  • lpush key value[value...] :向列表key中添加一个或多个元素value,每次往列表头添加,存在相同的value不去重,同样在列表头添加;
  • lrange key start stop :获取列表key中指定范围内的元素,列表头元素从0开始;
  • lrem key count value :从列表头开始,删除列表key中元素值为value的元素,删除的数量为count个;
  • lset key index value :设置列表key中下标位置为index的元素的值为value,其中下标是从列表头0开始,如果index超过了列表最后一个位置,会报ERR index out of range错误;
  • lpop key :移除列表第一个元素,返回值为移除的元素值;
  • rpop key :移除列表最后一个元素,返回值为移除的元素值;
  • blpop key timeout :移除并获取列表第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止,阻塞等待时间timeout以秒为单位;
  • brpop key timeout :移除并获取列表最后一个元素,同样和上面一个机制一样的;
  • llen key :获取列表key的长度。

哈希字典表(Hash):字典类型用来存储一组数据对。每个数据对又包含键和值两部分。也是两种方式实现:压缩列表和散列表,和列表使用压缩列表的情况类似的。
使用压缩列表的情况:
       ① 字典中保存的键和值的大小都要小于64字节;
       ② 字典中键值对的个数要小于512。

redis中对哈希字典存储的是键值对,常用命令:

  • hset name key value :设置name哈希表中的key value键值对;
  • hget name key :获取name哈希表中key的值value;
  • hgetall name :获取name哈希表中所有的键值对内容;
  • hsetnx name key value :和string的setnx命令是类似的;
  • hmset name key1 value1[key2 value2...] :向name哈希表中设置多个key value键值对;
  • hmget name key1[key2...] :根据key批量获取name哈希表中的value;
  • hlen name :获取name哈希表中的键值对数量;
  • hdel name key1[key2...] :删除name哈希表中指定的一个或多个key的键值对;
  • hexists name key :查询name哈希表中是否存在key的键值对,存储返回1,不存在返回0;
  • hkeys name :查询name哈希表中所有的键key。

集合(Set):集合用来存储一组不重复的数据。这种数据类型也有两种实现方法,一种是基于有序数组,另一种是基于散列表。
使用有序数组的情况:
       ①存储的数据都是整数
       ②存储的元素个数小于512。

redis中对集合类型的常用操作命令:

  • sadd key member1[member2...] :向集合key中添加一个或多个元素;
  • srem key member1[member2...] :移除集合一个或多个成员;
  • scard key :获取集合的成员数量;
  • sdiff key1[key2...] :获取给定所有集合的差集,这里差集指的是最前面的集合key1有的元素,但是在后面的key2...所有集合没有的元素,看下图执行命令的截图,当集合只有一个时就返回该集合的所有成员;
  • sdiffstore destination key1[key2...] :将计算出来的差集保存到destination的集合中;
  • sinter key1[key2...] :获取给定所有集合的交集,这个好理解就不做实例截图了;
  • sinterstore destination key1[key2...] :将计算出来的差集保存到destination的集合中;
  • sunion key1[key2...] :获取给定所有集合的并集;
  • sunionstore destination key1[key2] :将计算出来的交集保存到destination的集合中;
  • sismember key member :判断merber是否是集合key中的成员,是则返回1,否返回0;
  • smembers key :返回集合key中的所有成员;
  • smove source destination member :将成员member从source集合移到destination集合中,如果source中没有member成员,则返回0,移动成功返回1;
  • spop key :移除并返回集合中的一个随机元素;

有序集合(SortSet):用来存储一组数据,并且每个数据会附带一个得分。也存在两种实现方式:压缩列表跳表
使用压缩列表的情况:
       ①所有数据的大小都要小于64字节;
       ②元素个数要小于128个。

有序集合在实际的工作当中引用很广泛,特别是排行榜类的场景,下面的命令也可以针对性的考虑到实际的引用场景中来加深理解:

  • zadd key score1 member1[socre2 member2...] :向有序集合key中添加一个或者多个带分值score的成员member,如果score不存在或者不存则会报错;
  • zrange key start stop[withscores] :通过指定score的值从start到stop的区间内中所有的成员,如果后面追加withscores则结果集中会返回每个成员的score值;
  • zcard key :获取有序集合key的所有成员数;
  • zincrby key increment member :对有序集合key中的member成员的分值score进行追加increment数量,并返回想加后的值;
  • zcount key min max :计算有序集合key中分数在min和max区间中的成员个数;
  • zlexcount key min max :计算有序集合key中成员min和max区间中的成员个数,注意和上面的就别,请看操作截图;
  • zrange key start stop[withscores] :返回有序集合key中索引(注意索引和分数的区别,索引指的是在有序集合的下标位置,从0开始)在start和stop之间的所有成员,如果后面追加withscores则结果集中会返回每个成员的score值;
  • zrank key member :获取有序集合key中member成员所在集合中的索引位置;
  • zrem key member1[member2...] :移除有序集合key中一个或多个成员;
  • zremrangebylex key min max :移除命令,命令中有lex,其实就可以想到是指成员字段的min和max区间了;
  • zremrangebyrank key start stop :移除有序集合key排名在start和stop区间的成员;
  • zremrangebyscore key min max :移除有序集合key分数在min和max区间的成员;
  • zrevrange key start stop[withscores] : 获取从start到stop区间的排名,分数从高到底,其中最高的start从0开始;
  • zrevrangebyscore key max min[withscores] :获取分数在max到min区间的,分数从高到底,注意这里是max到min;
  • zrevrank key member :获取有序集合key中成员member所在集合中的排名;
  • zscore key member :获取分数。

       接下来我们分别来详细了解一下压缩列表跳表,其他的底层用的数据结构:双向循环链表、散列表和有序数组这些就不说啦,这些是非常常见的数据结构,可能散列表比较少见,但是参考HashMap的底层实现,就可以很快了解散列表的思想和底层原理。

二、压缩列表和跳表详解

压缩列表(ziplist):

       压缩列表是redis设计的一种数据存储结构,有点类似数组,和数组相同点:都是通过分配一片连续的内存空间来存储数据的,不同点:压缩列表每个元素的数据大小可以不一样,并且支持存储不同类型的数据,而数组是连续的内存空间和相同类型大小的数据结构。从不同点来分析,”压缩“也就是体现在可以根据每个元素大小针对性分配空间,我们知道数组要求每个元素类型相同,大小也相同,但是如果用数组存储相同类型但不同大小的数据时,比如存储不同长度字符串的话,那每个数组元素所需要的内存空间大小就必须是所有元素大小中最大的那个(有点拗口),这样就难免存在内存空间的浪费。

       在这里多讲一下数组为什么需要存储相同类型和大小的数据(其实这里说的大小是数组给每个元素所分配的空间大小,而不是指存储单个数据的实际大小)并且在内存分配一块连续的内存空间,目的就是能保证随机访问。数组的查询之所以是O(1)复杂度,就是因为数组的特性,我们假设开辟一个数组后被分配在内存的起始地址偏移量为startOffset,每个元素分配的空间为unitSize,那我们要访问下标为 i 的元素就可以直接通过计算得到该元素所在内存的位置从而访问对应元素的值:startOffset + i * unitSize。其实大家到这里也可以间接的领悟到为什么前人设计数组的下标访问是从0开始的,假设如果我们从1开始,那也就意味着随机访问寻找对应的元素地址的公式就为:startOffset + (i-1)*unitSize,这样的话每次cpu在执行的过程中都会增加一次i-1的操作,当然在小规模的遍历访问下没什么,但是当遍历数量非常大的数组的时候,总共对i-1的操作执行时间不会是个小数目,虽然微乎其微但是伟大的前人还是追求极致的性能,值得佩服和感叹!

       而压缩列表之所以节省空间,就是因为可以根据每个元素实际的大小来开辟相应空间来存储。看下图应该可以一目了然:现在我们来看下压缩列表的底层实现,在这篇文章我对底层的原理讲述只是从一个粗犷的视角(非代码层面的视角)去给大家讲述redis压缩列表实现的原理,这只能帮助大家先从逻辑思维层次上去学习原理,有助于大家对源码的理解,如果真正要学透某个底层数据结构的实现原理,最好的方法就是去啃源码https://www.2cto.com/kf/201604/497968.html该文章可以阅读详细源码)!先不去细究它底层详细的数据结构源码的定义,我们先从全局的视角看下ziplist的存储结构(下面所有图片来源都来自于参考文献):


三大块区域:ziplist header(压缩列表头部区域)  ziplist nodes(压缩列表主体节点区域) end mark(压缩列表在内存空间结尾的标志)
end mark:1字节,值固定为255,压缩列表尾部的占位符,表示压缩列表到这里就结束了。
ziplist header :三个字段的空间分配:zlbytes、zltail、zllen

zlbytes:4字节无符号整型,存储的值代表整个zplist所在内存空间占用的字节数。※大家zlbytes这个值有哪些用处吗?思考一下,文章后面会给出

zltail:4字节无符号整型,存储的是ziplist nodes区域中最后一个节点的偏移值,也就是我们可以通过计算:ziplist起始地址 +zltail 得到最后一个节点在内存中的起始地址,从而快速访问到压缩列表最后一个节点的内容。另外还有好处就是如果我们对压缩列表进行在尾部的添加和删除操作,就不需要从头节点遍历到尾部再进行操作。

zllen:2字节无符号整型,存储的是ziplist的ziplist nodes部分中节点总个数。

ziplist nodes:存储的就是一个个元素了,我们也从全局视角看下每个node的存储结构,分三个区域:

prev_entry_length:用来存储该节点前面一个节点的长度,很容易可以想到通过这个值可以通过指针运算直接访问到前面一个节点,结合前面的zltail就可以做到从后往前遍历整个压缩列表。长度可能是1字节或者5字节。1字节:如果前一个节点的长度小于254的时候就会只开辟1字节的空间来存储前一节点的长度。5字节:如果前一个节点的长度大于或等于254时使用5个字节存储,其中这5个字节的第1个字节的数值固定为254,用来标识前一个节点的长度值大于等于254,后面4个则用来存储前一个节点实际的长度值。
encoding & cur_entry_length:存储的是编码类型encoding和当前节点的长度cur_entry_length。
data:真正存放数据的空间。其类型由第二部分的encoding和cur_entry_length决定。

       其中encoding & cur_entry_length这个区域不是很好理解,该区域存在三种长度的可能,分别是1字节、2字节和5字节(这里如果有对字节还不够了解的先去查阅一下资料哈,不然后面的内容更不好理解了)。其中encoding域占用字节最高的2个bit(位),这样encoding就有四种不同的取值,分别是00、01、10、11。redis规定如下:如果encoding域的取值为11,表示整型编码,意味着该节点数据域(data)存储的是一个整型值类型。如果encoding域的取值为00、01和10,表示字符串编码,意味着该节点数据域存储的是一个字符串类型。除了最高的2个位,后面的全部用来表示data的长度。看下图(网上找的图):

我们现在来看看整体的压缩列表结构图:
 


压缩列表的级联扩展:

       我们来讲下级联扩展这样一个概念和其产生原因。到这里大家肯定知道压缩列表是一块连续的内存空间,这样的话某个数据节点向前和向后遍历访问其他节点就是通过指针计算的方式,例如从p节点访问前一个节点就是使用记录的prev_entry_length来计算前一个节点的位置,访问后一个节点是通过自己节点的长度cur_entry_length来计算下一个节点的位置。在向前遍历压缩列表的时候,我们用到了prev_entry_length,前面说过这个字段长度会根据前一个节点的长度来确定是1字节还是5字节,那设想一下这样一种情况:我们在某个节点p前面添加一个新的节点s时,节点长度大于254,而原来p节点记录的前一个节点的长度是小于254,所以prev_entry_length的长度是1字节,但是新增s之后就意味着p节点需要进行空间扩展,prev_entry_length从一字节扩展到5字节,从而会影响整个p节点的长度,而恰巧一种极端的情况就是p节点后面所有的节点保存的前一个节点的长度prev_entry_length的值都在临界的250到254之间,这样的话从p节点空间扩展后,就会波及了后面所有的节点都需要进行空间扩展,这就是压缩列表的级联扩展,从我的分析来看这种情况发生的概率是非常低的!其实除了添加元素的时候,删除元素也会影响到节点的空间调整,大家不妨思考思考,redis对这种因插入节点的长度较小而引起的缩小操作采取“不处理”的策略。

       redis压缩列表就讲到这啦,文末给的参考文献非常不错,里面贴了详细注解的源码,大家有兴趣可以去研读!下面我们来学习redis的跳表底层实现。


跳表(skipList)

       跳表主要是为了解决普通单向链表、双向链表和循环链表的通病:查询效率问题,我们通过研究底层的实现来看看是如何提高查询效率的。redis是用c语言实现的,跳表涉及两个重要的结构体:跳表的节点和跳表,话不多说先上源码,然后我们再看图(跳表内容参考博客,原文链接:https://blog.csdn.net/WhereIsHeroFrom/article/details/84997418,该博客讲的非常透彻,里面有完整源,强烈推荐):

跳表的节点结构体(详细看注释):

typedef struct zskiplistNode {  /*跳表的节点结构体,代表每一个节点内容 */
    robj *obj;                              /*节点实际存储的数据 */
    double score;                           /*每个节点的分数,用来排序 */
    struct zskiplistNode *backward;         /*指向前一个节点的指针 */
    struct zskiplistLevel {                 /*当前节点的层的内容,每一层也就对应着跳表的层 */
        struct zskiplistNode *forward;      /*当前层中指向的下一个节点的指针 */ 
        unsigned int span;                  /*跨度,从当前节点到指向的节点,需要跨过几个节点 */
    } level[];
} zskiplistNode;

       这里详细解释一下几个字段属性的内容,后续有助于看结构图和源码;
       obj:就是指向存储数据的指针;
       score:节点的分数,通过前面的redis相关命令介绍,大家应该已经知道在向有序集合添加原始的时候,会设置分数值,就是这个值,score和obj就是进行排序的依据的,score为排序的第一维度,obj是第二维度,下文源码会体现出来;
       level[]:数组,很关键是源码最难理解的地方,这也就是实现跳跃的底层结构。数组的下标表示的就是层数。其中每一层里的forward是指向下一个节点的指针,span不太好理解,和forward配合使用就是从当前节点到达forward指向的节点所需要的长度,也就跨度。

跳表结构体

typedef struct zskiplist { /*跳表结构体,代表一个跳表 */
    struct zskiplistNode *header, *tail;   /*有头结点和尾节点的指针 */
    unsigned long length;                  /*跳表的节点个数 */
    int level;                             /*跳表的层数 */
} zskiplist;

       header和tail:指向头结点和尾节点的,结合下图理解;
       length:跳跃表中总的结点个数,不过除了头结点
       level:表示该跳跃表最大的层数,初始值为1,代表0层,0层也就是普通的链表啦。

接下来我们看下跳表的图示(图片来源就在前面链接里 ):

       上图代表了一个三个元素的跳跃表。其中绿色格子是 zskiplist 部分,蓝色格子是 zskiplistNode 部分。图片从下往上看是内存递增的方向,即 绿色 header 代表跳跃表的首地址, 蓝色 obj 代表跳跃表结点的首地址。
       当跳跃表中元素为 n 个时,其实有 n+1 个结点,多出来的那个结点就是跳跃表的头结点,头结点的 score 值为 0,obj 置 NULL,backward 后退指针指向 NULL,并且默认有 32 个层 level[0...31]。
       绿色部分:整型值 level 代表除了头结点以外,其它结点的层高的最大值(这里为 4 ,表示该跳表目前有四层);length 表示实际元素个数(这里为 3);tail 指向跳跃表的尾结点;header 指向跳跃表的头结点(固定不变)。
       蓝色部分:每个跳跃表结点都有一个后退指针 backward,用来指向链表结构中的前一个结点;而 level [] 数组的每个元素是一个由前进指针 forward 和 跨度span 组成的 zskiplistLevel 结构。除了头结点外,其它结点的层高是在这个结点创建的时候随机出来的,(score,obj)则是用来对跳跃表进行排序的排序依据。
       红色曲线:代表每个结点在当前层的 forward 指针,这个指针一定是指向一个结点的首地址,而非 zskiplistLevel 结构的地址。
       橙色数字:代表每个结点在当前层指向的结点到当前结点的跨度 span。这个跨度的计算很容易从图中看出,如果把这个跳跃表横向理解成一个数组,那么跨度就代表红色曲线两头的两个结点的 Rank(接下来会介绍 Rank 的含义)之差。

跳表相关操作源码:

1、创建跳跃表

zskiplistNode *zslCreateNode(int level, double score, robj *obj) { // 创建跳表节点方法
    zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel)); /*开辟空间  */
    zn->score = score; // 指定分数
    zn->obj = obj; // 指定节点内容
    return zn;
}
 
zskiplist *zslCreate(void) { // 创建跳表方法
    int j;
    zskiplist *zsl;              // 声明一个跳表结构体指针
    zsl = zmalloc(sizeof(*zsl)); // 分配空间                        
    zsl->level = 1;              // 初始层数为1               
    zsl->length = 0;             // 初始元素个数为0,但其实有一个头结点,前文说过
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); // 创建头结点,level是最大值32,分数为0,不指向任何内容null
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) { // 初始化头结点的每层的内容,总共有32层
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    zsl->header->backward = NULL; // 将头结点中指向前一个节点的属性字段赋值null
    zsl->tail = NULL;             // 尾节点目前也是空
    return zsl;
}

       上图为一个跳跃表的初始结构。

2、插入节点

       跳跃表的插入有点类似链表,首先要找到一个插入位置,生成一个结点,然后修改插入位置的指针进行插入操作。结点插入的 API 为 zslInsert,整个插入过程分为以下四部分:

        a、寻找插入位置;

        b、随机插入结点层数;

        c、生成插入结点并插入;

        d、额外信息更新;

zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;
    serverAssert(!isnan(score));
 
    /************************* 寻找插入位置 *************************/
    x = zsl->header;     // 首先从头结点开始
    for (i = zsl->level-1; i >= 0; i--) {   // 注意:这里从最高层开始找
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        while (x->level[i].forward &&
          (x->level[i].forward->score < score ||
            (x->level[i].forward->score == score &&
              compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    /************************* 寻找插入位置 *************************/
 
    /*********************** 随机插入结点层数 ***********************/
    level = zslRandomLevel();
    if (level > zsl->level) {
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            update[i] = zsl->header;
            update[i]->level[i].span = zsl->length;
        }
        zsl->level = level;
    }
    /*********************** 随机插入结点层数 ***********************/
 
    /*********************** 生成结点并插入  ***********************/
    x = zslCreateNode(level,score,obj);
    for (i = 0; i < level; i++) {
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
    /*********************** 生成结点并插入  ***********************/
 
    /*********************** 额外信息更新   ***********************/
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    zsl->length++;
    /*********************** 额外信息更新   ***********************/
    return x;
}

3、删除节点

int zslDelete(zskiplist *zsl, double score, robj *obj) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    int i;
    /************************* a、寻找待删除结点 *************************/
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                (x->level[i].forward->score == score &&
                compareStringObjects(x->level[i].forward->obj,obj) < 0)))
            x = x->level[i].forward;
        update[i] = x;
    }
    x = x->level[0].forward;
    /************************* a、寻找待删除结点 *************************/
 
    /************************* b、执行结点的删除 *************************/
    if (x && score == x->score && equalStringObjects(x->obj,obj)) {
        zslDeleteNode(zsl, x, update);
        zslFreeNode(x);
        return 1;
    }
    /************************* b、执行结点的删除 *************************/
 
    return 0;
}


三、文中思考题

1、在列表、hash表、有序集合中,为什么数据量小的时候会选择压缩列表存储结构?

       这是一个最优问题的思考,我们思考的方向首先就是要清楚明白每种数据结构的特性,使之在存储数据以及对数据的操作(增删改)时体现出不同的特性,然后再根据不同的特性结合存储的数据量、数据类型甚至计算机底层的运行机制等等多方面因素来考虑用哪种数据结构更优,接下来我们来分析分析。
       我们先来分析压缩列表链表,redis的列表(List)和有序集合(SortSet)就是在这两种中选择的,列表用的是双向循环链表,有序集合用的是跳表(底层也是链表的实现原理)。

压缩列表特性

       分配连续的内存空间;并且根据数据的实际大小进行空间分配。这是两个最大的特性,前文也讲过并且和数组进行了对比。我们先来看分配连续的内存空间的优缺点:

优点:
       1)对CPU缓存友好:不知道大家对CPU执行的时候,数据的来源的流程,CPU在运行的时候如果高速缓存(假设你对这个机制的前后都了解)没有对应的数据,就会去内存中读取(这里假定内存一定存在,不然还涉及到内存到磁盘中的流程),读取的流程是将内存的数据读取到高速缓存中,但是CPU读取数据不只是单单从特定的内存地址读取所需要的数据到高速缓存,而是读取对应内存连续的一块内存数据(具体大小没有深究),到这里大家是否就明白了对CPU缓存友好的原因了吧。
        2)便于查找:这个也是因为1),因为CPU缓存在一次内存数据读取后有连续的数据,那查找的时候就会减少对内存的读取,另外就是便于遍历,压缩列表查找是通过前和后的遍历,只要通过指针计算就可以向前和向后一个数据的查找,而数组更加容易查找,通过O(1)的复杂度就可以查到,这也是压缩列表比数组劣势的地方。

缺点:
       存在不能分配足够空间的可能:分配连续的内存空间首先前提就是内存需要有足够大小并且连续的内存空间,这样方可实现,但是内存往往因为底层淘汰机制内存的清理等会导致某些时候整块内存空间中有很多零碎的内存空闲空间,当需要一个较大并连续的内存空间时往往无法满足。

       根据数据的实际大小进行空间分配就是为了节省内存空间,但是我们分析压缩列表,确实在一些场景下会大大节省空间,比如整个压缩列表中节点的大小都参差不齐,大的又非常大小的又非常小,但是当每个节点的大小都非常接近或者说都相等时,其实还浪费了空间,因为压缩列表每个节点还要分配空间来存储前一个节点的长度和当前节点的长度。接下来我们看下链表。

链表的特性:

       不需要连续的内存空间,通过指针指向其它节点,不管这个节点在内存的哪个位置,这样内存不管有多少零碎的空闲空间,只要总的空闲内存足够就可以实现链表的空间分配;同样每个元素节点的大小都根据实际的需要分配空间。

       但是缺点就是查询数据的时候只能通过一个个节点(当然是对普通链表来说,跳表就是针对这个来进行优化的)遍历的方式,O(n)的时间复杂度。

       综合上面的分析,我们基本明白列表和有序集合这两类数据,redis为什么数据量小的时候会选择压缩列表进行存储数据了,主要考虑的还是内存空间查询效率上。

       我们分析下Hash表中压缩列表和散列表,压缩列表如果存储非常大的数据量的话,一方面不好分配足够大的连续的内存空间;其次由于他的查找方式也是通过类似于链表的遍历方式,在数据量非常大的时候不便于查找;
另外就是数据量非常大的时候,其实也是浪费了空间,还是因为每个节点都要存储前一个节点和当前节点的长度值。其实结合散列表来考虑的话,我觉得redis之所以从压缩列表转换成散列表,最主要的还是查询效率上的考虑吧。集合(Set)这个数据类型用到的有序数组和散列表其实也是这方面的考虑。

四、总结

       本篇文章我们主要从分析redis数据类型:String、List、Hash、Set、SortSet这几常用的数据类型,包括对这些数据类型常用的操作命令,再到分析对应数据类型底层使用的数据结构,关键的两个数据结构就是压缩列表跳表,我们也分别详细的学习这两个数据结构的底层实现和原理相关内容,通过研读源码的方式来学习数据结构是最有效的方法,所以希望大家多看源码。最后咱们分析不同数据结构的不同特性,思考在实际的引用场景使用哪种数据结构更优,这篇文章就到这里,感谢看到最后的你!

五、参考文献

https://blog.csdn.net/whereisherofrom/category_7762123.html

https://blog.csdn.net/WhereIsHeroFrom/article/details/84997418

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值