Redis的基石(一) —— ziplist

1,151 阅读7分钟

redis对外开放的api中包含了很多数据结构。比如list,set,hash,zset,string...然而实际上这只是封装后对外的结果。即使同样是list,在数据量不同的情况下,redis内部会使用不同的数据结构来实现。

这次讲的是压缩列表。属于redis内部的一种数据结构。设计它的初衷是什么? redis的定位是缓存,这决定了它会有极大的QPS,如何提高它的性能呢,数据终归会落于磁盘上,数据量越大,检索数据的耗时就会越长。可以考虑将热点数据保留在缓存中,这是很容易想到的。然而内存是有限的,如何确保在内存中只存储有效的数据呢? 第一点涉及到它的内存淘汰策略。基于lru,lfu算法将非热点数据从内存中移除,提高热点数据在内存中的占比。 第二点通过压缩算法在有限的内存中存储尽量多的数据。lucene也是同样的思路,并且lucene的索引也做到尽可能小,以便直接加载到内存中,提升查询效率。

ziplist就是基于第二点设计的。 ziplist的数据结构是这样 ... 第一个32位对应zlbytes存储的是ziplist的总大小 第二个32位对应zltail存储的是tailentry的偏移量 (zlend前的最后一个entry被称为tailentry) 第三个16位对应zllen存储的是entry数量 zlend表示一个结束标识,值为255

entry的数据结构是这样 prevlen记录的是上一个entry的长度。当上一个entry的长度小于254时才用一个字节来记录 当上一个entry的长度超过254时使用5个字节来记录(第一个字节是255表示prevlen需要5个字节来表示,之后的4个字节记录的是真正的长度)

encoding记录的是data的编码方式。当data本身是int类型判断int需要多少位来表示,encoding会被映射成对印的值。当data是string类型时,根据data的长度需要多少位来表示,转换成字节后作为encoding。

data记录的就是实际的数据。只针对int类型进行压缩.比如此时int值为-128~127,实际上只需要一个字节就能表示,就能节省3字节。

实际上ziplist无法对字符串进行压缩,只适合存储一些int类型的数据。当k/v占用的基础内存开销比较大的时候不适合使用这种结构。redis内部会自动选择合适的数据结构实现list

下面贴一下核心方法的注释

一些宏

/* Return total bytes a ziplist is composed of. */
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))

/* Return the offset of the last item inside the ziplist. */
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))

/* Return the length of a ziplist, or UINT16_MAX if the length cannot be
 * determined without scanning the whole ziplist. */
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))

/* The size of a ziplist header: two 32 bit integers for the total
 * bytes count and last item offset. One 16 bit integer for the number
 * of items field. */
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))

/* Size of the "end of ziplist" entry. Just one byte. */
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))

/* Return the pointer to the first entry of a ziplist. */
#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)

/* Return the pointer to the last entry of a ziplist, using the
 * last entry offset inside the ziplist header. */
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))

/* Return the pointer to the last byte of a ziplist, which is, the
 * end of ziplist FF entry. */
#define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)

#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do {                     \
    ZIP_DECODE_PREVLENSIZE(ptr, prevlensize);                                  \
    if ((prevlensize) == 1) {                                                  \
        (prevlen) = (ptr)[0];                                                  \
    } else if ((prevlensize) == 5) {                                           \
        assert(sizeof((prevlen)) == 4);                                        \
        memcpy(&(prevlen), ((char*)(ptr)) + 1, 4);                             \
        memrev32ifbe(&prevlen);                                                \
    }                                                                          \
} while(0);

ziplistNew 创建一个ziplist

unsigned char *ziplistNew(void) {
    // 一开始分配的数组大小就是head+end
    // head大小为 2个int32 + 1个int16
    // end大小为 一个int8
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    unsigned char *zl = zmalloc(bytes);
    // head中第一个int32 记录的就是此时ziplist的总大小
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    // head中第二个int32 记录的是tail块的起始位置 因为此时内部还没有任何数据 所以header的末尾就是tail块的起始位置
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    // head中第一个int16记录此时ziplist中有效数据的总长度 也就是不包含head, end
    ZIPLIST_LENGTH(zl) = 0;
    // ziplist最后一个元素对应int8 存储的是一个固定值255
    zl[bytes-1] = ZIP_END;
    return zl;
}

ziplistPush 将某个字符串插入到ziplist的头部或者尾部

unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {
    unsigned char *p;
    // p 对应应当插入的位置 因为头部是固定不变的所以 需要定位到数组[0]+headsize的位置
    // ZIPLIST_ENTRY_END 就是总长度-1 也就是对应到end的位置
    p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
    // 将s字符串插入到zl中
    return __ziplistInsert(zl,p,s,slen);
}

__ziplistInsert 插入实现

unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    // 第一个元素是总大小 也就是包含head+end
    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; /* initialized to avoid warning. Using a value
                                    that is easy to see if for some reason
                                    we use it uninitialized. */
    zlentry tail;

    /* Find out prevlen for the entry that is inserted. */
    // 代表本次希望插入到头部 (且此时ziplist已经存在数据)  在下面会解析第一个entry的prevlen 不过第一个entry没有前prev prevlen长度应该是0
    if (p[0] != ZIP_END) {
        ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
    } else {
        // 代表本次是希望插入到末尾 或者想要插入头部但此前没有数据 这样逻辑是一样的 都是找到tail的位置
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
        // 代表此前有数据  如果没有数据head的尾部就是end同时也是tail的起始位置 3者重叠
        if (ptail[0] != ZIP_END) {
            // 这里是在计算tail的长度
            prevlen = zipRawEntryLength(ptail);
        }
    }

    /* See if the entry can be encoded
     * 对本次即将插入的数据进行编码
     * */
    if (zipTryEncoding(s,slen,&value,&encoding)) {
        /* 'encoding' is set to the appropriate integer encoding */
        reqlen = zipIntSize(encoding);
    } else {
        /* 'encoding' is untouched, however zipStoreEntryEncoding will use the
         * string length to figure out how to encode it.
         * 本次长度超过了int 无法压缩 占用的字节数等于字符串的字节数
         * */
        reqlen = slen;
    }
    /* We need space for both the length of the previous entry and
     * the length of the payload.
     * 除了表示数据本身需要的长度外 还需要空间存储前一个entry的长度以及encoding
     * 如果本次是第一次插入数据 prevlen为0这时reqlen会返回1
     * */
    reqlen += zipStorePrevEntryLength(NULL,prevlen);
    reqlen += zipStoreEntryEncoding(NULL,encoding,slen);

    /* When the insert position is not equal to the tail, we need to
     * make sure that the next entry can hold this entry's length in
     * its prevlen field. */
    int forcelarge = 0;
    // p[0] != ZIP_END 代表本次期望插入头部  这里是确保当前p用于存储上一个entry长度信息的prevlen有足够的空间表示本次新entry的长度信息
    // 简化情况第一次p[0]就是end 此时diff是0
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
    // 目前头部entry前缀长度必然是0 也就是 zipPrevLenByteDiff(p,reqlen) 不会是负数 应该不会出现下面的情况
    if (nextdiff == -4 && reqlen < 4) {
        nextdiff = 0;
        forcelarge = 1;
    }

    /* Store offset because a realloc may change the address of zl.
     * 这里得到的是从zl起始位置到tailentry起始位置的偏移量(如果ziplist为空tailentry起始位置就是end)
     * */
    offset = p-zl;
    // 首先要尝试对ziplist进行扩容 确保有足够的空间
    zl = ziplistResize(zl,curlen+reqlen+nextdiff);
    p = zl+offset;

    /* Apply memory move when necessary and update tail offset.
     * 本次插入头部且原本有数据
     * */
    if (p[0] != ZIP_END) {
        /* Subtract one because of the ZIP_END bytes
         * param1 dest param2 src param3 len
         * 先简化情况 认为nextdiff为0 这里是把从firstentry开始的数据移动到了本次新entry插入后的位置
         *
         * 如果本次 nextdiff为4 (没有其他情况了)
         * 以下操作等同于        memmove(p+reqlen+nextdiff,p,curlen-offset-1);
         * */
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);

        /* Encode this entry's raw length in the next entry. 如何出现??? */
        if (forcelarge)
            zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
        else
            // 将新entry的长度信息填充到原first的首位
            zipStorePrevEntryLength(p+reqlen,reqlen);

        /* Update offset for tail
         * tailoffset在原基础上增加reqlen(实际上可能还需要再移动nextdiff)
         * */
        ZIPLIST_TAIL_OFFSET(zl) =
            intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);

        /* When the tail contains more than one entry, we need to take
         * "nextdiff" in account as well. Otherwise, a change in the
         * size of prevlen doesn't have an effect on the *tail* offset.
         * 读取原first的数据
         * */
        zipEntry(p+reqlen, &tail);
        // 如果此时原first已经是最后一个entry了 tailoffset只需要增加reqlen 否则还要加上本次first为存储prevlen额外申请的nextdiff
        if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
        }
        // 先简化情况 认为ziplist中没有entry 此时进入的是这个分支
    } else {
        /* This element will be the new tail. */
        // 此时将tail_offset的位置标记为end 那么之后就应该会在end后插入新的entry作为tailentry
        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
    }

    /* When nextdiff != 0, the raw length of the next entry has changed, so
     * we need to cascade the update throughout the ziplist
     * 如果当前entry为存储prevlen申请了额外内存
     * */
    if (nextdiff != 0) {
        offset = p-zl;
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }

    // 从以下代码可以反推entry的结构 首先第一部分记录的是prevlen 第二部分对应encoding标记(针对数据是否可用int表示以及使用多少位表示 会设置不同的标记)
    // 第三部分存储被压缩的数据 注意str是不支持压缩的

    /* Write the entry 此时将数据填充到entry中 */
    p += zipStorePrevEntryLength(p,prevlen);
    // 将压缩标记写入到p(entry)中
    p += zipStoreEntryEncoding(p,encoding,slen);
    // 发现采用的压缩方式是str 也就是无法压缩 直接拷贝数据
    if (ZIP_IS_STR(encoding)) {
        memcpy(p,s,slen);
    } else {
        // 针对int类型通过压缩使得消耗的bit尽可能小
        zipSaveInteger(p,value,encoding);
    }

    // 因为插入了一个新的entry 所以len+1  ziplist_len 代表当前压缩列表中总计有多少entry
    ZIPLIST_INCR_LENGTH(zl,1);
    return zl;
}

zipRawEntryLength 返回当前entry的总长度

unsigned int zipRawEntryLength(unsigned char *p) {
    unsigned int prevlensize, encoding, lensize, len;

    // prevlen 可能占用1字节或者5字节 当prevlen超过254时 就会占用5字节
    ZIP_DECODE_PREVLENSIZE(p, prevlensize);
    // 这里是解析entry长度
    ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
    return prevlensize + lensize + len;
}

zipTryEncoding 计算encoding的值

    long long value;

    if (entrylen >= 32 || entrylen == 0) return 0;
    if (string2ll((char*)entry,entrylen,&value)) {
        /* Great, the string can be encoded. Check what's the smallest
         * of our encoding types that can hold this value.
         * 如果值在一个特殊的范围内 使用ZIP_INT_IMM_MIN+value来表示 相当于在标记位内部直接存储了value
         * 读取其他大小的数据时 必须先读取encoding之后再读取位数
         * */
        if (value >= 0 && value <= 12) {
            *encoding = ZIP_INT_IMM_MIN+value;
            // 代表能够用8bit表示
        } else if (value >= INT8_MIN && value <= INT8_MAX) {
            *encoding = ZIP_INT_8B;
        } else if (value >= INT16_MIN && value <= INT16_MAX) {
            *encoding = ZIP_INT_16B;
        } else if (value >= INT24_MIN && value <= INT24_MAX) {
            *encoding = ZIP_INT_24B;
        } else if (value >= INT32_MIN && value <= INT32_MAX) {
            *encoding = ZIP_INT_32B;
        } else {
            *encoding = ZIP_INT_64B;
        }
        *v = value;
        return 1;
    }
    return 0;
}

zipIntSize 根据encoding的值判断本次int需要多少字节存储

unsigned int zipIntSize(unsigned char encoding) {
    switch(encoding) {
    case ZIP_INT_8B:  return 1;
    case ZIP_INT_16B: return 2;
    case ZIP_INT_24B: return 3;
    case ZIP_INT_32B: return 4;
    case ZIP_INT_64B: return 8;
    }
    // 代表value的范围在 0~12之间 所以value的值被直接存储在了encoding中
    if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX)
        return 0; /* 4 bit immediate */
    panic("Invalid integer encoding 0x%02X", encoding);
    return 0;
}

zipStorePrevEntryLength 用prevlen表示传入的长度需要多少字节

unsigned int zipStorePrevEntryLength(unsigned char *p, unsigned int len) {
    if (p == NULL) {
        // 在前一个entry长度小于big长度(254)时 使用1来表示 否则为1+int的长度(4)
        return (len < ZIP_BIG_PREVLEN) ? 1 : sizeof(len)+1;
    } else {
        // p不为空 代表将数据填充到p的后面  反推p(entry)的结构 第一部分是prevlen 也就是上一个entry的长度
        if (len < ZIP_BIG_PREVLEN) {
            p[0] = len;
            return 1;
        } else {
            return zipStorePrevEntryLengthLarge(p,len);
        }
    }
}

zipStoreEntryEncoding encoding需要多少字节来表示

unsigned int zipStoreEntryEncoding(unsigned char *p, unsigned char encoding, unsigned int rawlen) {
    // len代表buf中占用了多少个槽
    unsigned char len = 1, buf[5];

    // 当encoding为0 也是进入这个分支
    if (ZIP_IS_STR(encoding)) {
        /* Although encoding is given it may not be set for strings,
         * so we determine it here using the raw length.
         * 4*16-1 = 63 可以用(2的6次-1)表示
         * */
        if (rawlen <= 0x3f) {
            if (!p) return len;
            // 这里2^6|rawlen 刚好可以用6位表示
            buf[0] = ZIP_STR_06B | rawlen;
            // 一个char最多表示 0xff 这里就代表需要占用buf的2个槽
        } else if (rawlen <= 0x3fff) {
            len += 1;
            if (!p) return len;
            buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
            buf[1] = rawlen & 0xff;
        } else {
            // 剩余的数据统一占用5个槽
            len += 4;
            if (!p) return len;
            buf[0] = ZIP_STR_32B;
            buf[1] = (rawlen >> 24) & 0xff;
            buf[2] = (rawlen >> 16) & 0xff;
            buf[3] = (rawlen >> 8) & 0xff;
            buf[4] = rawlen & 0xff;
        }
    } else {
        /* Implies integer encoding, so length is always 1.
         * 代表本次数据可以用int来表示 encoding统一占用1字节
         * */
        if (!p) return len;
        buf[0] = encoding;
    }

    /* Store this length at p. */
    memcpy(p,buf,len);
    return len;
}

zipSaveInteger 采用压缩方式存储int

void zipSaveInteger(unsigned char *p, int64_t value, unsigned char encoding) {
    int16_t i16;
    int32_t i32;
    int64_t i64;
    // 使用一个char来表示  一个char能够表示8位 此时int的值不超过8位
    if (encoding == ZIP_INT_8B) {
        ((int8_t*)p)[0] = (int8_t)value;
    } else if (encoding == ZIP_INT_16B) {
        i16 = value;
        memcpy(p,&i16,sizeof(i16));
        memrev16ifbe(p);
    } else if (encoding == ZIP_INT_24B) {
        i32 = value<<8;
        memrev32ifbe(&i32);
        // 这里要去掉一开始 <<8 的操作
        memcpy(p,((uint8_t*)&i32)+1,sizeof(i32)-sizeof(uint8_t));
    } else if (encoding == ZIP_INT_32B) {
        i32 = value;
        memcpy(p,&i32,sizeof(i32));
        memrev32ifbe(p);
    } else if (encoding == ZIP_INT_64B) {
        i64 = value;
        memcpy(p,&i64,sizeof(i64));
        memrev64ifbe(p);
        // 这种情况数据直接存储在encoding里了 所以不需要处理
    } else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) {
        /* Nothing to do, the value is stored in the encoding itself. */
    } else {
        assert(NULL);
    }
}

完整注释:github.com/a137872798/…