Redis高级之String底层源码(1)-String和Hash数据类型的底层结构

        请读者知悉:本章内容涉及到底层数据结构,不建议初学者阅读。建议写了解我写的上一篇内容:Redis基础-掌握各种数据类型及使用场景-CSDN博客后在阅读本章。本章适合那些在工作中熟练使用Redis并对每种数据类型底层的数据结构感兴趣的同学,另外,本文会涉及一部分Redis源码讲解。

工具版本:redis-6.0.20

                 CLion 2023.1

1 概述

        用户Redis的人都知道,Redis提供了一个逻辑上的对象系统,构建了一个键值对(k-v)数据库,以供客户端使用。这个对象系统包括字符串对象、哈希对象、列表对象、集合对象和有序集合对象等,但是Redis面向内存中并没有直接使用这些对象,而是使用了简单的动态字符串、链表、字典、跳跃表、整数集合和压缩列表这些数据结构。下图是五种常用的类型结构底层数据类型:

2 String数据结构底层解析

        简单动态字符串(SDS)是Redis的基本数据结构之一,用于存储字符串和整数数据,保证二进制安全。这意味着Redis字符串可以包含任何类型的数据。比如图像等,不过字符串的最大长度限制是其存储容量不能超过512MB。

        Redis默认并未直接使用C字符串,而是以Struct的形式构造了一个SDS的抽象类型。当Redis需要一个可以被修改的字符串时,就会使用SDS来表示。在Redis数据库中,包含字符串值的键值对都是有SDS实现的(Redis中所有的键都是由字符串对象实现的,即底层是由SDS实现的,所有的值对象中包含的字符串对象底层也是有SDS实现)。

2.1 String的三种编码

        字符串对象的内部编码有三种:int、raw、embstr。Redis会根据当前值的类型和长度来决定使用哪种编码。

        如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性中(将void转换成long),并将字符串对象的编码设置为int,存储8字节的长整型(long,2的63次方-1)。

        另外还有一个条件:把value当作字符串来看,它的长度不能超过20.这种编码类型是为了节省内存。Redis默认会缓存10000个整型值,意味着如果有多个不同的键,其值的个数在10000以内,事实上都会共享同一个对象。

【例2.1.1】查看String类型底层数据结构:

        raw编码存储大于44字节的字符串(3.2版本之前是39字节),需要分配两次内存空间(分别是Redis  Object 和SDS分配空间)。

        raw编码的结构图如下:

        embstr格式的SDS存储小于44个字节的字符串,只分配一个内存空间(因为Redis Object和SDS是连续的),结构如下图:

        embstr编码是专门用于保存短字符串的一种优化编码方式。embstr和raw编码都会使用SDS来保存值,不同之处在于embstr会通过调用一次内存分配函数分配一块连续的内存空间来保存RedisObject和SDS,而RAW编码会调用两次内存分配函数分配两块空间来保存RedisObject和SDS。Redis这样做有很多好处:

 (1)embstr 降低了内存分配次数

(2)embstr编码只需要调用一次内存释放函数

(3)embstr编码数据在一块连续的内存里面可以更好地利用CPU缓存提升性能。

(4)embstr和raw这两种内部编码的长度限制是44字节。

1.embstr编码底层的源码如下:

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
#限制对象编码长度
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}

具体执行结果如下:

        为什么它们的长度限制是44字节?

        embstr分配的是一块连续的内存空间,包含redisObject和SDS。redisObject占用16字节,存储结构的源码如下:

typedef struct redisObject {
    /*对象的类型*/
    unsigned type:4;

    /* 具体的数据结构,即raw,embstr*/
    unsigned encoding:4;

    /*24位,与内存回收有关*/
    /* LRU time (relative to global lru_clock) or
     * LFU data (least significant 8 bits frequency
     * and most significant 16 bits access time). */
    unsigned lru:LRU_BITS; 

    /*占4个字节,引用计数,当refcount为0时,表示该对象已经不被任何对象引用,可以进行垃圾回收*/
    int refcount;

    /*占8个字节对象实际的数据结构*/
    void *ptr;
} robj;

 经过计算:type + encoding + lru:LRU_BITS = 32位 = 4字节。

所有redisObject的大小 = 4 + 4 + 8 = 16字节。

2.SDS有多种结构(sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64),用户存储不同长度的字符串,分别代表2的5次方 = 32byte,2的8次方 = 256byte等。

以sdshdr8举例:

struct __attribute__ ((__packed__)) sdshdr8 {
    /* 已使用的空间长度 */
    uint8_t len; 
     /* 剩余可用空间的长度 */
    uint8_t alloc;
    unsigned char flags;
    /*数据空间*/
    char buf[];
};

下面的公式计算SDS的头部信息占用的空间的大小

sdshdr8 = uint8_t (1字节)  * 2 + char(1字节) = 3字节

        需要说明的是,SDS结构最小的应该是SDS_TYPE_8(SDS_TYPE_5默认转成 8)。

if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;

最终 embstr = redisObject(16字节) + sds{char(n字节) + 3字节 + 1 字节(\0结束符)} = 64

n = 64 - 3 -1 -16 = 44

        所以一旦超过44字节,整体大小就会超过64字节,在Redis中将认为是一个大的字符串,不在使用embstr形式存储,存储结构将变为raw。

2.2 SDS和内存重新分配

        SDS内存分配流程如下:

下面是追加字符串的源码:

//追加字符串
sds sdscatlen(sds s, const void *t, size_t len) {
    //当前字符串的长度
    size_t curlen = sdslen(s);
    //按需调整空间
    s = sdsMakeRoomFor(s,len);
    //内存不足,返回null
    if (s == NULL) return NULL;
    //追加目标字符串到字节数组中
    memcpy(s+curlen, t, len);
    //设置追加后的长度
    sdssetlen(s, curlen+len);
    //追加结束符
    s[curlen+len] = '\0';
    return s;
}

 涉及到的方法如下:

//空间调整,只是调整空间,后续需要自己组装字符串
sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    //当前剩下的空间
    size_t avail = sdsavail(s);
    size_t len, newlen, reqlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;

    //空间足够时
    if (avail >= addlen) return s;
    //长度
    len = sdslen(s);
    //真正的数据体
    sh = (char*)s-sdsHdrSize(oldtype);
    //新分配的长度
    reqlen = newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    //小于1MB,2倍扩容
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        //大于1MB时,扩容1MB
        newlen += SDS_MAX_PREALLOC;
    //获取SDS结构类型
    type = sdsReqType(newlen);
    //如果type = 5,默认转成8
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    //头长度
    hdrlen = sdsHdrSize(type);
    assert(hdrlen + newlen + 1 > reqlen);  /* Catch size_t overflow */
    if (oldtype==type) {
        //长度够用并且数据结构不变
        newsh = s_realloc(sh, hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        //长度不够,重新申请内存
        newsh = s_malloc(hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);
        s_free(sh);
        s = (char*)newsh+hdrlen;
        s[-1] = type;
        sdssetlen(s, len);
    }
    sdssetalloc(s, newlen);
    return s;
}

        SDS_MAX_PREALLOC的容量大小默认是1MB,通过上面的源码可以看出,扩容策略是字符串在长度小于SDS_MAX_PREALLOC之前,扩容空间用加倍策略,也就是分配100%冗余空间;当长度超过SDS_MAX_PREALLOC(1MB)时,为了避免加倍后的 冗余空间过大而导致浪费,每次扩容只会多分配SDS_MAX_PREALLOC(1MB)大小的冗余空间。

        但是总空间不能超过512MB,当存储的内容超过512MB时,将直接报错。

2.3 embstr编码的内容追加

        值得注意的是,embstr是只读的,当再次对内容进行追加时,不管内容是否超过44字节都会直接把embstr编码变成raw编码。

2.4 内存空间释放

        为了提升性能,减少申请内存的开销,SDS提供了不需要直接释放内存的方法,而是通过重置统计值来实现清空,这个方法将SDS的LEN归零,此处的buf并没有真正被清除,只是新的数据可以覆盖掉旧的数据,而不需要重新申请内存。

void sdsclear(sds s) {
    sdssetlen(s, 0);
    s[0] = '\0';
}

         SDS内存释放(即惰性空间释放)示意图如下:

        在字符串操作中,Redis通过空间预分配和惰性空间释放的策略,在一定程度上减少了内存重分配的次数。这种策略会造成一定的内存浪凡,因此Redis SDS API提供了相应的API让我们在需要的时候可以真正释放掉SDS未使用的内存。

void sdsfree(sds s) {
    if (s == NULL) return;
    //直接释放内存
    s_free((char*)s-sdsHdrSize(s[-1]));
}

 2.5 SDS特征

        1、SDS二进制安全

        C字符串中的字符必须符合某一种编码,并且除了字符串的末尾外,字符串里面不能包含空字符,否则会被误认为是字符串结尾,这些限制使得C字符串只能保存文本格式的数据,而不是保存图片、音频、视频等二进制格式的数据。如“abc\0123”,C字符串的函数会把其中的“\0”当做字符串的结束符来处理,因此会忽略掉后面的“123”。SDS的buf字节数组不是字符数组,而是一系列二进制数的数组,SDS的API会以二进制的方式来处理buf数组里的数据,使用len属性的值来判断数组是否结束。

        2、时间复杂度

        (1)获取字符串长度:时间复杂度为O(1)。

        (2)获取SDS铲毒,提供了len属性,时间复杂度为O(1)。

        (3)获取SDS未使用空间的长度:提供了free属性,时间复杂度为O(1)。

        (4)清除SDS保存的内容:采用惰性空间分配策略,时间复杂度为O(1)。

        (5)创建一个长度为n的字符串:时间复杂度为O(n)。

        (6)拼接一个长度为n的SDS字符串:时间复杂度为O(n)。

3 Hash数据结构底层解析

        使用Redis的大部分用户应该看到过“尽可能使用Hash”这样的话,Hash可以把数据编码在很小的空间中,因此应尽可能使用Hash来表示数据。比如,当用户在web应用程序中有代表用户的对象时,不要为用户对象中的名称、电子邮件、密码等属性使用不同的键,而应为所有这些必填的属性使用单个Hash对象。

        Hash对象有两种实现方式:ZipList(压缩列表)和HashTable(哈希表)。

3.1 ZipList数据结构

        ZipList不是基础数据结构,时Redis自己设计的一种数据存储结构。有点类似数组,通过一片连续的内存空间来存储数据。与数组不同的是,它允许所存储的列表元素所占的内存空间不同。说到压缩,第一时间可能想到的就是节省内存。之所以说这种存储结构节省内存,是相对数组而言。数组要求每个元素存储空间的大小相同,如果要存储不同长度的字符串,就要以做大长度的字符串所占的存储空间作为字符串数据每个元素存储空间的大小,假如是50字节,因此在字符数数值中存储小于50字节长度的字符串就会浪费掉一部分存储空间。

        数组的优势就是占用一片连续的空间,可以很好地利用CPU缓存快速访问数据。如果既想保留数组的这种优势又想节省存储空间,那么可以对数组进行压缩。如下图所示:

        不过有一个问题,在遍历压缩列表的时候,我们并不知道每个元素所占的内存大小,因此无法计算出下一个元素的具体起始位置。如果能给每一个元素标注长度,就可以解决这个问题,如下图所示。

        接下来看一下Redis是如何通过实现ZipList使之结合即保留数组的有点又节省了内存。ZipList的结构如下图:

        下面具体分析每个属性。

(1)zlbytes:压缩列表的字节长度,是uint32_t类型,占4字节,因此压缩列表最多有2的32次方-1,可以通过该值计算zlend的位置,也可以在内存重新分配的时候使用。

(2)zltail:压缩列表尾部元素相对于压缩列表起始地址的偏移量,是uint32_t类型,占4字节,可以通过该值快速定位到列表尾部元素的位置。

(3)zllen:压缩列表元素的个数,是uint16_t类型,占2字节,表示最多存储的元素个数为65535,如果需要计算总数量,则需要遍历整个压缩列表。

(4)entryx:压缩列表存储的元素,既可以是字节数组,也可以是整数,长度不限。

(5)zlend:压缩列表的结尾,是uint8_t类型,占1个字节,恒为0xFF.。

(6)previous_entry_Length:表示当前一个元素的字节长度,占1字节或者5字节,当前元素的长度小于254时用1字节,大于等于254时用5字节,previous_entry_Length字段的第一个字节固定是0xFF,后面的4字节才是真正的前一个元素的长度。

(7)encoding:表示当前元素的编码,有整数或者字节数。为了节省内存,encoding字段长度可变。

(8)content:表示当前元素的内容。

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_END_SIZE        (sizeof(uint8_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)

        encoding进行特殊编码以节省空间:

/* 对上一个元素进行编码并将其写入指针p中,如果指针为空,则返回编码此长度所需的字节数
 * 1.len < 254 prevlen占用1字节,并写入当前entry的第一个字节
 * 2.len >= 254 prevlen占5个字节*/
static unsigned int zipmapEncodeLength(unsigned char *p, unsigned int len) {
    if (p == NULL) {
        return ZIPMAP_LEN_BYTES(len);
    } else {
        if (len < ZIPMAP_BIGLEN) {
            p[0] = len;
            return 1;
        } else {
            p[0] = ZIPMAP_BIGLEN;
            memcpy(p+1,&len,sizeof(len));
            memrev32ifbe(p+1);
            return 1+sizeof(len);
        }
    }
}

3.1.1 ZipList的编码

        ZipList中所有编码的说明如下:

        相关源码如下:

#define ZIP_STR_MASK 0xc0
#define ZIP_INT_MASK 0x30
#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)
#define ZIP_INT_16B (0xc0 | 0<<4)
#define ZIP_INT_32B (0xc0 | 1<<4)
#define ZIP_INT_64B (0xc0 | 2<<4)
#define ZIP_INT_24B (0xc0 | 3<<4)
#define ZIP_INT_8B 0xfe

        可以根据encoding字段第一个字节的前两位来判断content字段存储的是整数还是字节数组。当content存储的是字节数组时,后面字节标识标识的是字节数组的实际长度;当content存储的是整数时,根据第3、4位才能判断整数的具体类型;当encoding字段表示当前元素存储的是0~12的时,数据就直接存储在encoding字段最后4位,此时没有content字段。

3.1.2 遍历ZipList

        ZipList有两种遍历方式:一种是从头部到尾部,另一种是从尾部到头部。遍历ZipList的API定义如下,函数输入参数zl指向压缩列表的首地址,p指向当前访问元素的首地址;ziplistNext函数返回后面一个元素萨的首地址,ziplistPrev函数反水前面一个元素的首地址。

unsigned char *ziplistNext(unsigned char *zl, unsigned char *p)

unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p)

        ZipList每个元素的previous_entry_length字段存储的是前一个元素的长度,所以压缩列表从后和向前遍历相对简单,表达式(previous_entry_length)就可以获取前一个元素的首地址。从前和向后遍历时,则需要解码当前元素,计算当前元素长度才能获取后一个元素首地址,多了一步解码的步骤,ziplistNext源码:

unsigned char *ziplistNext(unsigned char *zl, unsigned char *p) {
    ((void) zl);

    if (p[0] == ZIP_END) {
        return NULL;
    }

    p += zipRawEntryLength(p);
    if (p[0] == ZIP_END) {
        return NULL;
    }

    return p;
}

3.1.3 创建ZipList

        创建ZipList的函数无输入参数,返回参数为压缩列表的首地址。

unsigned char *ziplistNew(void) {
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    unsigned char *zl = zmalloc(bytes);
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    ZIPLIST_LENGTH(zl) = 0;
    zl[bytes-1] = ZIP_END;
    return zl;
}

        上面创建了一个空的压缩列表,对zlbytes、zltail、zllen、zlend字段进行初始化。

3.1.4 在ZipList中插入新元素

        在ZipList中插入元素的函数其输入参数zl(压缩列表首地址)、p(指向新元素的插入位置)、s(数据内容)、slen(数据长度),返回压缩列表的首地址。

unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {
    unsigned char *p;
    p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
    return __ziplistInsert(zl,p,s,slen);
}

/* 向压缩列表插入一个元素 */
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen;
    unsigned int prevlensize, prevlen = 0;
    size_t offset;
    int nextdiff = 0;
    unsigned char encoding = 0;
    long long value = 123456789;
    zlentry tail;

    /* 查找所插入元素的上一个元素的长度 */
    if (p[0] != ZIP_END) {
        ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
    } else {
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
        if (ptail[0] != ZIP_END) {
            //编码
            prevlen = zipRawEntryLength(ptail);
        }
    }

    /* 查看元素是否可以编码 */
    //检查entry的value值是否可以编码为long类型,如果可以就把值保存在value中
    //并把所需最小字节长度保存在encoding中
    if (zipTryEncoding(s,slen,&value,&encoding)) {

        reqlen = zipIntSize(encoding);
    } else {
        /* 如果不能被编码,则zipEncodeLength方法将使用字符串的长度,以确定如何对其编码*/
        reqlen = slen;
    }
    /* 需要空间来容纳前一个元素的长度和当前元素的有效载荷长度 */
    reqlen += zipStorePrevEntryLength(NULL,prevlen);
    reqlen += zipStoreEntryEncoding(NULL,encoding,slen);

    /* 当插入元素的位置不是尾部时,需要确保下一个元素的前置字段可以容纳此元素的长度 */
    int forcelarge = 0;
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
    if (nextdiff == -4 && reqlen < 4) {
        nextdiff = 0;
        forcelarge = 1;
    }

    /* reqlen是zlentry所需大小,nextdiff是带插入位置元entry所需存储空间的大小差值 */
    offset = p-zl;
    //重新分配空间
    zl = ziplistResize(zl,curlen+reqlen+nextdiff);
    p = zl+offset;

    /* 应用内存移动病更新尾部偏移 */
    if (p[0] != ZIP_END) {
        /*原数据向后移动,腾出空间写入新的zlentry */
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);


        if (forcelarge)
            zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
        else
            zipStorePrevEntryLength(p+reqlen,reqlen);

        /* 更新尾部的偏移量 */
        ZIPLIST_TAIL_OFFSET(zl) =
            intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);

        zipEntry(p+reqlen, &tail);
        /*如果原插入位置的entry不是最后的tail元素,需要调整ZIPLIST_TAIL_OFFSET的值*/
        if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
        }
    } else {
        /* 当前这个元素将成为新的尾部 */
        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
    }

    /* 如果nextdiff不是0,需要循环更新后续entry中的prelen字段,需要在整个ziplist中级联更新 */
    if (nextdiff != 0) {
        //如果nextdiff不是0,需要循环更新后续entry中的prelen字段,最差情况下,所有entry都需要更新一遍
        offset = p-zl;
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }

    /* 给新的entry赋值 */
    p += zipStorePrevEntryLength(p,prevlen);
    p += zipStoreEntryEncoding(p,encoding,slen);
    if (ZIP_IS_STR(encoding)) {
        memcpy(p,s,slen);
    } else {
        zipSaveInteger(p,value,encoding);
    }
    ZIPLIST_INCR_LENGTH(zl,1);
    return zl;
}

        从上看出,当给ZipList新增元素时需要执行三个非常重要的函数,zipRawEntryLength函数通过previous_entry_length字段、encoding字段和content字段的内容计算出前一个元素的长度,prevlen的值。ziplistResize函数重新分配内存空间(重新申请一块更大的连续内存),将ZipList占用的内存空间(字节数)扩大,原来的数据依然保留在压缩列表的前段。第三个函数memmove重新分配空间后,需要将插入位置后的元素移动到指定位置,将新元素插入对应的位置。

3.1.5 在ZipList中删除元素

        在ziplist中删除元素与删除数组中的元素类型。

/* 删除元素 */
unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
    unsigned int i, totlen, deleted = 0;
    size_t offset;
    int nextdiff = 0;
    zlentry first, tail;
    //计算被删除元素所占的内存大小
    zipEntry(p, &first);
    for (i = 0; p[0] != ZIP_END && i < num; i++) {
        p += zipRawEntryLength(p);
        deleted++;
    }/* 循环结束后,p 指向需要删除的所有元素的下一个元素的位置*/
    
    //totlen表示需要删除的内存字节数
    totlen = p-first.p; 
    if (totlen > 0) {
        if (p[0] != ZIP_END) {
            
            nextdiff = zipPrevLenByteDiff(p,first.prevrawlen);

            p -= nextdiff;
            zipStorePrevEntryLength(p,first.prevrawlen);

            /* 将前置元素的长度编码进p中*/
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);

            zipEntry(p, &tail);
            if (p[tail.headersize+tail.len] != ZIP_END) {
                ZIPLIST_TAIL_OFFSET(zl) =
                   intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
            }

            /* 将偏移量p-zl+1 要删除的内存空间(字节数)在重新分配空间之前要复制数据*/
            memmove(first.p,p,
                intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
        } else {
          
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe((first.p-zl)-first.prevrawlen);
        }

       
        offset = first.p-zl;
        //重新分配内存空间
        zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
        ZIPLIST_INCR_LENGTH(zl,-deleted);
        //更新压缩列表的元素个数
        p = zl+offset;

        if (nextdiff != 0)
            zl = __ziplistCascadeUpdate(zl,p);
    }
    return zl;
}

        从上看出,删除元素同样需要三个重要步骤:

        (1)计算待删除元素的总长度

        (2)复制数据

        (3)重新分配内存空间。

        先复制数据,再重新分配内存空间 ,就是调用ziplistResize函数之前多余的那部分内存空间存储的数据已经被复制,然后回收这部分内容空间的时候就不会造成数据的丢失。

3.1.6 连锁更新

        当新增或删除数据时,后一个元素的存储位置会发生改变,而每一个元素的存储位置的改变都会造成重新分配内存空间和音符复制数据的操作,这种现象叫连锁更新。

previous_entry_length: 表示前一个元素的字节长度,当前一个字节的长度小于254时占1个字节,大于或等于254时占5字节。

entry3元素的previous_entry_length占1字节,因为entry2元素的长度小于254。当删除entry2后,则entry3的前一个元素变成entry1,而entry1的长度大于254,所以entry3元素的previous_entry_length占5字节,从而造成了元素空间扩容,而entry3后面的元素可有可能会造成扩容,以此压缩列表需要重新分配内存空间。

由于本章内容较多,分两篇写完。本篇到此结束,后面继续写关于HashTable、set、zset三种类型的数据结构。喜欢就关注吧

              

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

geminigoth

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值