zoukankan      html  css  js  c++  java
  • Redis源码解析05: 压缩列表

      压缩列表(ziplist)是列表键和哈希键的底层实现之一。当列表键只包含少量列表项,并且每个列表项要么是小整数值,要么是长度较短的字符串时;或者当哈希键只包含少量键值对,并且每个键值对的键和值要么是小整数值,要么是长度较短的字符串时,那么Redis就会使用压缩列表来做为列表键或哈希键的底层实现。 

      压缩列表是Redis为了节约内存而开发的,可用于存储字符串和整数值。它是一个顺序型数据结构,由一系列特殊编码的连续内存块组成。一个压缩列表可以包含任意多个结点(entry),每个entry的大小不定,每个entry可保存一个字符串或一个整数值。

      ziplist的相关实现在都在ziplist.c中。

    一:ziplist结构

      ziplist的结构如下:

      <zlbytes><zltail><zllen><entry1><entry2>...<entryN><zlend>

      zlbytes是一个uint32_t类型的整数,表示整个ziplist占用的内存字节数;

      zltail是一个uint32_t类型的整数,表示ziplist中最后一个entry的偏移量,通过该偏移量,无需遍历整个ziplist就可以确定尾结点的地址;

      zllen是一个uint16_t类型的整数,表示ziplist中的entry数目。如果该值小于UINT16_MAX(65535),则该属性值就是ziplist的entry数目,若该值等于UINT16_MAX(65535),则还需要遍历整个ziplist才能得到真正的entry数目;

      entryX表示ziplist的结点,每个entry的长度不定;

      zlend是一个uint8_t类型的整数,其值为0xFF(255),表示ziplist的结尾。

      在ziplist.c中,定义了一系列的宏,可以分别获取ziplist中存储的各个属性,比如:

    #define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))  
    #define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))       
    #define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))     
    #define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))         
    #define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)    
    #define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))      
    #define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)    

      ZIPLIST_BYTES获取ziplist中的zlbytes属性;ZIPLIST_TAIL_OFFSET获取ziplist的zltail属性;ZIPLIST_LENGTH获取ziplist的zllen属性

      ZIPLIST_ENTRY_HEAD得到ziplist头结点的地址;ZIPLIST_ENTRY_TAIL得到ziplist中尾节点的首地址,ZIPLIST_ENTRY_END得到ziplist结尾字节zlend的地址。注意,ziplist中所有的属性值都是以小端的格式存储的。因此取得ziplist中保存的属性值后,还需要对内存做字节翻转才能得到真正的值。intrev32ifbe就是在大端系统下对内存进行字节翻转的函数。

    二:entry结构

      每个压缩列表节点都由previous_entry_length、encoding和content三部分组成。

      previous_entry_length表示前一个entry的字节长度,根据该字段值就可以从后向前遍历ziplist。previous_entry_length字段长度可以是1字节,也可以是5字节。如果前一个entry长度小于254字节,则该字段只占用一个字节;如果前一个entry长度大于等于254字节,则该字段占用5个字节:第一个字节置为0xFE(254),之后的4个字节保存entry的实际长度(小端格式存储)。用0xFE(254)这个值作为分界点,是因为0xFF(255)被用作ziplist的结束标志,一旦扫描到0xFF,就认为ziplist结束了。

      content字段保存节点的实际内容,它可以是一个字符串或者整数,值的类型和长度由encoding属性决定。

      encoding字段记录了节点的content所保存的数据类型及长度。如果entry中保存的是字符串,则encoding字段的前2个二进制位可以是00、01和10,分别表示不同长度类型的字符串,剩下的二进制位就表示字符串的实际长度;如果entry中的内容为整数,则encoding字段的前2个二进制位都为11,剩下的2个二进制位表示整数的类型。encoding的形式如下:

      00pppppp,1字节,表示长度小于等于63(2^6- 1) 字节的字符串;

      01pppppp|qqqqqqqq,2 字节,表示长度小于等于16383(2^14 - 1) 字节的字符串;

      10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt,5字节,表示长度小于等于4294967295(2^32- 1) 字节的字符串

      11000000,1字节,表示int16_t类型的整数;  11010000,1字节,表示int32_t类型的整数;

      11100000,1字节,表示int64_t类型的整数;  11110000,1字节,表示24位有符号整数;

      11111110,1字节,表示8位有符号整数;  1111xxxx,1字节,表示0到12之间的值。使用这一编码的节点没有相应的oontont属性,因为xxxx就可以表示0到12之间的值。因为0000和1110不可用,所以xxxx的取值从0001到1101,也就是1到13之间的值。因此需要从xxxx减去1,才能得到实际的值。

      注意,所有的整数都是以小端的格式存储的

    三:连锁更新

      每个节点的previous_entry_length字段记录了前一个节点的长度。如果前一节点的长度小于254字节,那么previous_entry_length字段占用1个字节;如果前一节点的长度大于等于254字节,那么previous_entry_length字段占用5个字节。

      考虑这样一种情况:在一个压缩列表中,有多个连续的、长度介于250字节到253字节之间的节点e1至eN。因所有节点的长度都小于254字节,所以e1至eN节点的previous_entry_length字段都是1字节长。这时,如果将一个长度大于等于254字节的新节点new设置为压缩列表的表头节点,那么new将成为e1的前置节点,但是因为e1的previous_entry_length字段仅长1字节,没办法保存新节点new的长度,所以需要对压缩列表执行空间重分配操作,将e1节点的previous_entry_length字段从原来的1字节扩展为5字节。现在,麻烦的事情来了,原e1的长度介于250字节至253字节之间,e1的previous_entry_length字段变成5个字节后,e1的长度就大于等于254了。从而e2的previous_entry_length字段,也需要从原来的1字节长扩展为5字节长。因此,需要再次对压缩列表执行空间重分配操作,并将e2节点的previous_entry_length属性从原来的1字节长扩展为5字节长。

      以此类推,为了让每个节点的previous_entry_length属性都符合压缩列表对节点的要求,程序需要不断地对压缩列表执行空间重分配操作,直到eN为止。Redis将这种在特殊情况下产生的连续多次空间扩展操作称之为“连锁更新”(cascade update)。除了添加新节点可能会引发连锁更新之外,删除节点也可能会引发连锁更新。

      因为连锁更新在最坏情况下需要对压缩列表执行N次空间重分配操作,而每次空间重分配的最坏复杂度为O(N),所以连锁更新的最坏复杂度为O(N^2)。尽管连锁更新的复杂度较高,但它真正造成性能间题的几率是很低的。

      首先,压缩列表里要恰好有多个连续的、长度介于250字节至253字节之间的节点,连锁更新才有可能触发。在实际中,这种情况并不多见;其次,即使出现连锁更新,但只要被更新的节点数量不多,就不会对性能造成任何影响,比如对三五个节点进行连锁更新是绝对不会影响性能的;因此,ziplistPush等函数的平均复杂度仅为O(N)。在实际中,我们可以放心地使用这些函数,而不必担心连锁更新会影响压缩列表的性能。 

      ziplist变动时,previous_entry_length字段长度可能需要从1字节扩展为5字节,从而会引起连锁更新,也可能需要从5字节收缩为1字节。这就有可能会发生抖动现象,也就是节点的previous_entry_length字段,不断的扩展和收缩的现象。Redis中,为了避免这种现象,允许previous_entry_length字段在需要收缩的情况下,保持5字节不变。

    四:代码

      Redis中的ziplist实现,并未涉及难以理解的算法。但是因为ziplist本身的编码需求较多,因而代码需要处理各种细节,初看之下比较繁杂,分析之后,其实很容易理解。

     1:连锁更新

      下面就是处理连锁更新的代码,zl指向一个ziplist,p指向其中第一个不需要更新的节点(第一个已经更新过的节点),其后续的节点可能需要更新:

    static unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
        size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize;
        size_t offset, noffset, extra;
        unsigned char *np;
        zlentry cur, next;
    
        while (p[0] != ZIP_END) {
            cur = zipEntry(p);
            rawlen = cur.headersize + cur.len;
            rawlensize = zipPrevEncodeLength(NULL,rawlen);
    
            /* Abort if there is no next entry. */
            if (p[rawlen] == ZIP_END) break;
            next = zipEntry(p+rawlen);
    
            /* Abort when "prevlen" has not changed. */
            if (next.prevrawlen == rawlen) break;
    
            if (next.prevrawlensize < rawlensize) {
                /* The "prevlen" field of "next" needs more bytes to hold
                 * the raw length of "cur". */
                offset = p-zl;
                extra = rawlensize-next.prevrawlensize;
                zl = ziplistResize(zl,curlen+extra);
                p = zl+offset;
    
                /* Current pointer and offset for next element. */
                np = p+rawlen;
                noffset = np-zl;
    
                /* Update tail offset when next element is not the tail element. */
                if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
                    ZIPLIST_TAIL_OFFSET(zl) =
                        intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
                }
    
                /* Move the tail to the back. */
                memmove(np+rawlensize,
                    np+next.prevrawlensize,
                    curlen-noffset-next.prevrawlensize-1);
                zipPrevEncodeLength(np,rawlen);
    
                /* Advance the cursor */
                p += rawlen;
                curlen += extra;
            } else {
                if (next.prevrawlensize > rawlensize) {
                    /* This would result in shrinking, which we want to avoid.
                     * So, set "rawlen" in the available bytes. */
                    zipPrevEncodeLengthForceLarge(p+rawlen,rawlen);
                } else {
                    zipPrevEncodeLength(p+rawlen,rawlen);
                }
    
                /* Stop here, as the raw length of "next" has not changed. */
                break;
            }
        }
        return zl;
    }

      首先获取ziplist当前的长度curlen;然后从p开始轮训,获取p指向节点的总长度rawlen,以及编码该字节长度需要的字节数rawlensize;如果p指向的结点没有后继结点,直接退出返回;否则,next表示p的后继节点,next节点的previous_entry_length字段值为next.prevrawlen,该字段长度是next.prevrawlensize。如果next.prevrawlen与rawlen相等,则表示next节点的前继节点的长度未发生变化,直接退出返回; 

            如果next.prevrawlensize小于rawlensize,表示next节点的前继节点的长度,原来小于254字节,现在大于等于254字节了。因此next节点的previous_entry_length字段需要扩展长度,扩展的字节数extra为rawlensize-next.prevrawlensize,利用ziplistResize扩展ziplist的内存空间。注意,扩容前要保存p的偏移量,扩容后利用该偏移量,可以重新得到p的位置。

            如下图,分别表示扩容前和扩容后的情况:

      如果next节点不是尾节点,则需要更新ziplist的zltail属性;如果next是尾结点,因为next之前的内容没有变化,因此无需更新zltail属性;

      然后开始移动内存,移动的内容是,从next节点的previous_entry_length字段之后的内存开始,到ziplist末尾字节zlend之前的内容:memmove(np+rawlensize, np+next.prevrawlensize, curlen-noffset-next.prevrawlensize-1);然后更新next中的previous_entry_length字段为rawlen;然后p指向next节点,依次遍历下一个节点;        

      如果next.prevrawlensize大于rawlensize,表示next节点的前继节点的长度,原来大于等于254字节,现在小于254字节了。为了避免“抖动”,调用zipPrevEncodeLengthForceLarge保持next节点的previous_entry_length字段长度不变,并强制编码为rawlen,然后退出返回; 

      如果next.prevrawlensize等于rawlensize,表示next节点的前继节点的长度,原来小于254字节,现在还是小于254字节,或者原来大于等于254字节,现在还是大于等于254字节。这种情况下,直接将next节点的previous_entry_length字段编码为rawlen,然后退出返回。

  • 相关阅读:
    ComboBoxEdit 数据绑定 使用模板
    ObservableCollection
    ListView.MouseDoubleClick
    Style 的查找 FindResource
    OpenFileDialog
    ItemsControl
    下拉框比较符
    ListView 控件与 内容
    测试oracle数据库连接
    MySQL ERROR 1300 (HY000): Invalid utf8 character string
  • 原文地址:https://www.cnblogs.com/lovelaker007/p/8684095.html
Copyright © 2011-2022 走看看