zoukankan      html  css  js  c++  java
  • Redis底层数据结构之list

    Redis中另一个常用的数据结构就是list,其底层有linkedListzipListquickList这三种存储方式。

    链表linkedList

    Java中的LinkedList类似,Redis中的linkedList是一个双向链表,也是由一个个节点组成的。Redis中借助C语言实现的链表节点结构如下所示:

    //定义链表节点的结构体 
    typedf struct listNode{
        //前一个节点
        struct listNode *prev;
        //后一个节点
        struct listNode *next;
        //当前节点的值的指针
        void *value;
    }listNode;
    

    pre指向前一个节点,next指针指向后一个节点,value保存着当前节点对应的数据对象。listNode的示意图如下所示:

    链表的结构如下:

    typedf struct list{
        //头指针
        listNode *head;
        //尾指针
        listNode *tail;
        //节点拷贝函数
        void *(*dup)(void *ptr);
        //释放节点函数
        void *(*free)(void *ptr);
        //判断两个节点是否相等的函数
        int (*match)(void *ptr,void *key);
        //链表长度
        unsigned long len;
    }
    

    head指向链表的头节点,tail指向链表的尾节点,dup函数用于链表转移复制时对节点value拷贝的一个实现,一般情况下使用等号足以,但在某些特殊情况下可能会用到节点转移函数,默认可以给这个函数赋值NULL即表示使用等号进行节点转移。free函数用于释放一个节点所占用的内存空间,默认赋值NULL的话,即使用Redis自带的zfree函数进行内存空间释放。match函数是用来比较两个链表节点的value值是否相等,相等返回1,不等返回0。len表示这个链表共有多少个节点,这样就可以在O(1)的时间复杂度内获得链表的长度。

    链表的结构如下所示:

    zipList

    RediszipList结构如下所示:

    typedf struct ziplist<T>{
        //压缩列表占用字符数
        int32 zlbytes;
        //最后一个元素距离起始位置的偏移量,用于快速定位最后一个节点
        int32 zltail_offset;
        //元素个数
        int16 zllength;
        //元素内容
        T[] entries;
        //结束位 0xFF
        int8 zlend;
    }ziplist
    

    zipList的结构如下所示:

    注意到zltail_offset这个参数,有了这个参数就可以快速定位到最后一个entry节点的位置,然后开始倒序遍历,也就是说zipList支持双向遍历。

    下面是entry的结构:

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

    prelen保存的是前一个entry节点的长度,这样在倒序遍历时就可以通过这个参数定位到上一个entry的位置。encoding保存了content的编码类型。content则是保存的元素内容,它是optional类型的,表示这个字段是可选的。当content是很小的整数时,它会内联到content字段的尾部。entry结构的示意图如下所示:

    好了,那现在我们思考一个问题,为什么有了linkedList还有设计一个zipList呢?就像zipList的名字一样,它是一个压缩列表,是为了节约内存而开发的。相比于linkedList,其少了prenext两个指针。在Redis中,prenext指针就要占用16个字节(64位系统的一个指针就是8个字节)。另外,linkedList的每个节点的内存都是单独分配,加剧内存的碎片化,影响内存的管理效率。与之相对的是,zipList是由连续的内存组成的,这样一来,由于内存是连续的,就减少了许多内存碎片和指针的内存占用,进而节约了内存。

    zipList遍历时,先根据zlbyteszltail_offset定位到最后一个entry的位置,然后再根据最后一个entry里的prelen时确定前一个entry的位置。

    连锁更新

    上面说到了,entry中有一个prelen字段,它的长度要么是1个字节,要么都是5个字节:

    • 前一个节点的长度小于254个字节,则prelen长度为1字节;
    • 前一个节点的长度大于254字节,则prelen长度为5字节;

    假设现在有一组压缩列表,长度都在250~253字节之间,突然新增一个entry节点,这个entry节点长度大于等于254字节。由于新的entry节点大于等于254字节,这个entry节点的prelen为5个字节,随后会导致其余的所有entry节点的prelen增大为5字节。

    同样地,删除操作也会导致出现连锁更新这种情况,假设在某一时刻,插入一个长度大于等于254个字节的entry节点,同时删除其后面的一个长度小于254个字节的entry节点,由于小于254的entry节点的删除,大于等于254个字节的entry节点将会与后面小于254个字节的entry节点相连,此时就与新增一个长度大于等于254个字节的entry节点时的情况一样,将会发生连续更新。发生连续更新时,Redis需要不断地对压缩列表进行内存分配工作,直到结束。

    linkedList与zipList的对比

    • 当列表对象中元素的长度较小或者数量较少时,通常采用zipList来存储;当列表中元素的长度较大或者数量比较多的时候,则会转而使用双向链表linkedList来存储。
    • 双向链表linkedList便于在表的两端进行pushpop操作,在插入节点上复杂度很低,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还有额外保存两个指针;其次,双向链表的各个节点都是单独的内存块,地址不连续,容易形成内存碎片。
    • zipList存储在一块连续的内存上,所以存储效率很高。但是它不利于修改操作,插入和删除操作需要频繁地申请和释放内存。特别是当zipList长度很长时,一次realloc可能会导致大量的数据拷贝。

    quickList

    Redis3.2版本之后,list的底层实现方式又多了一种,quickListqucikList是由zipList和双向链表linkedList组成的混合体。它将linkedList按段切分,每一段使用zipList来紧凑存储,多个zipList之间使用双向指针串接起来。示意图如下所示:

    节点quickListNode的定义如下:

    typedf struct quicklistNode{
        //前一个节点
        quicklistNode* prev;
        //后一个节点
        quicklistNode* next;
        //压缩列表
        ziplist* zl;	
        //ziplist大小
        int32 size;		
        //ziplist 中元素数量
        int16 count;
        //编码形式 存储 ziplist 还是进行 LZF 压缩储存的zipList
        int2 encoding;			
        ...
    }quickListNode
    

    quickList的定义如下所示:

    typedf struct quicklist{
        //指向头结点
        quicklistNode* head;
        //指向尾节点
        quicklistNode* tail;
        //元素总数
        long count;
        //quicklistNode节点的个数
        int nodes;	
        //压缩算法深度
        int compressDepth;		
        ...
    }quickList
    

    上述代码简单地表示了quickList的大致结构,为了进一步节约空间,Redis还会对zipList进行压缩存储,使用LZF算法进行压缩,可以选择压缩深度。

    每个zipList可以存储多少个元素

    想要了解这个问题,就得打开redis.conf文件了。在DVANCED CONFIG下面有着清晰的记载。

    # Lists are also encoded in a special way to save a lot of space.
    # The number of entries allowed per internal list node can be specified
    # as a fixed maximum size or a maximum number of elements.
    # For a fixed maximum size, use -5 through -1, meaning:
    # -5: max size: 64 Kb  <-- not recommended for normal workloads
    # -4: max size: 32 Kb  <-- not recommended
    # -3: max size: 16 Kb  <-- probably not recommended
    # -2: max size: 8 Kb   <-- good
    # -1: max size: 4 Kb   <-- good
    # Positive numbers mean store up to _exactly_ that number of elements
    # per list node.
    # The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
    # but if your use case is unique, adjust the settings as necessary.
    list-max-ziplist-size -2
    

    quickList内部默认单个zipList长度为8k字节,即list-max-ziplist-size的值设置为-2,超出了这个阈值,就会重新生成一个zipList来存储数据。根据注释可知,性能最好的时候就是就是list-max-ziplist-size-1-2,即分别是4kb和8kb的时候,当然,这个值也可以被设置为正数,当list-max-ziplist-szie正数n时,表示每个quickList节点上的zipList最多包含n个数据项。

    压缩深度

    上面提到过,quickList中可以使用压缩算法对zipList进行进一步的压缩,这个算法就是LZF算法,这是一种无损压缩算法,具体可以参考上面的链接。使用压缩算法对zipList进行压缩后,zipList的结构如下所示:

    typedf struct ziplist_compressed{
        //元素个数
        int32 size;
        //元素内容
        byte[] compressed_data
    }
    

    此时quickList的示意图如下所示:

    当然,在redis.conf文件中的DVANCED CONFIG下面也可以对压缩深度进行配置。

    # Lists may also be compressed.
    # Compress depth is the number of quicklist ziplist nodes from *each* side of
    # the list to *exclude* from compression.  The head and tail of the list
    # are always uncompressed for fast push/pop operations.  Settings are:
    # 0: disable all list compression
    # 1: depth 1 means "don't start compressing until after 1 node into the list,
    #    going from either the head or tail"
    #    So: [head]->node->node->...->node->[tail]
    #    [head], [tail] will always be uncompressed; inner nodes will compress.
    # 2: [head]->[next]->node->node->...->node->[prev]->[tail]
    #    2 here means: don't compress head or head->next or tail->prev or tail,
    #    but compress all nodes between them.
    # 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
    # etc.
    list-compress-depth 0
    

    list-compress-depth这个参数表示一个quickList两端不被压缩的节点个数。需要注意的是,这里的节点个数是指quicklist双向链表的节点个数,而不是指ziplist里面的数据项个数。实际上,一个quicklist节点上的ziplist,如果被压缩,就是整体被压缩的。

    • quickList默认的压缩深度为0,也就是不开启压缩
    • list-compress-depth为1,表示quickList的两端各有1个节点不进行压缩,中间结点进行压缩;
    • list-compress-depth为2,表示quickList的首尾2个节点不进行压缩,中间结点进行压缩;
    • 以此类推

    从上面可以看出,对于quickList来说,其首尾两个节点永远不会被压缩。

    总结

    参考

    图解redis五种数据结构底层实现

    你确定不来了解一下Redis列表的内部原理

    Redis 3.2版本后list的实现-quickList

    Redis深度历险:核心原理和应用实践

  • 相关阅读:
    UPDATE 时主键冲突引发的思考【转】
    MySQL Sandbox安装使用
    主从复制1062错误解决方法
    InnoDB log file 设置多大合适?
    EXPLAIN 命令详解
    【机器学习】ID3算法构建决策树
    连续属性离散化处理
    【机器学习】决策树基础知识
    【机器学习】模型评估与选择
    【机器学习】单层感知器
  • 原文地址:https://www.cnblogs.com/reecelin/p/13358432.html
Copyright © 2011-2022 走看看