Redis 5.0数据结构之压缩列表ziplist源码详解(一)

由于篇幅过长,ziplist的源码分析分为两篇,本篇是第一篇,涉及到ziplist的数据结构、初始化创建、以及ziplist的特殊编码部分的源码分析。

第二篇博客地址:Redis 5.0数据结构之压缩列表ziplist源码详解(二)

概述

Redis基于C语言开发,由于C并未内置链表,因此Redis自己实现了一系列链表,有双端链表linkedlist、压缩列表ziplist、快速链表quicklist等,本篇将从源码角度展现压缩列表ziplist。

笔者相关文章推送:
Redis 5.0数据结构之双端链表linkedlist源码解析
Redis 链表list常用命令总结

Redis 中的list实现

众所周知Redis有五种数据类型,链表是其中的一种,在Redis在3.2版本,修改了list的底层实现,在Redis 3.2之前链表是由双端链表linkedlist和压缩链表ziplist共同实现的,Redis3.2版本之后主要实现优化为了快速链表quicklist。

Redis3.2之前,当同时满足以下两个条件时,列表键的底层实现为ziplist,其余情况均使用linkedlist

  • 哈希对象保存的所有键值的字符串长度小于64字节;
  • 哈希对象保存的键值对数量小于512个;

可见压缩列表主要是为了节约内存开发。

本篇使用源码版本均为Redis 5.0,其他版本也具有一定的参考价值,主要介绍的压缩列表ziplist源码均来自ziplist.h和ziplist.c两个文件。

压缩列表的构成

笔者认为ziplist是链表中十分特殊的存在。传统意义上或是说更广为人知的list链表实现,都是在节点构造中使用指针,然后直接使用该指针指向当前节点前后节点的地址,来实现链表。但ziplist并不是使用指针直接保存节点内存地址的方式实现,ziplist使用连续内存地址+偏移量的方式实现链表。

理解压缩列表及其源码的重点就在于要理解压缩列表的结构。压缩列表没有数据结构代码定义,压缩列表是一种通过内存特殊编码方式实现的数据结构

压缩列表相关宏定义函数

首先源码中提供了一些宏定义函数,用于获取ziplist相关信息,在下文的源码分析中各方法将频繁出现

/* ziplist结尾特殊标志位 默认0xFF 即255 */
#define ZIP_END 255

/* 返回压缩列表ziplist包含的总字节数 */
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))

/* 返回ziplist中最后一个节点的偏移量 */
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))

/* 返回ziplist的总长度
 * 该属性值超过UINT16_MAX(65536)时,返回UINT16_MAX,表示真实长度需要遍历得出 */
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))

/* 返回ziplist的header长度(header总所需字节数) 总共10字节 header包括
 * 一个32位的属性用于表示ziplist的总字节数
 * 一个32位的属性用于表示起始地址到尾节点的偏移量
 * 一个16位的属性用于表示节点数,即ziplist长度 */ 
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))

/* 返回ziplist结尾标志的长度(结尾所需字节数) 总共1字节 */
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))

/* 返回ziplist中指向第一个节点的指针 */
#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)

/* 返回ziplist中指向最后一个节点的指针 使用header中的的偏移量获取 */
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))

/* 返回指向ziplist结尾标识(0xFF)的指针 */
#define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)

压缩列表创建函数

由于压缩列表基于内存特殊编码实现,源码种并没有数据结构代码定义,但可以通过上文的宏定义函数,结合压缩列表的创建函数来推出压缩列表的大致组成部分。压缩列表创建函数如下:

/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
	// 为压缩列表申请bytes字节空间
    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);
    // 设置len属性为0 len表示ziplist中节点数量
    ZIPLIST_LENGTH(zl) = 0;
    // 设置ziplist结尾标志0xFF
    zl[bytes-1] = ZIP_END;
    return zl;
}

压缩列表组成部分

由源码可见压缩列表共有如下五个组成部分:

属 性类 型长 度用 途
zlbytesunit32_t4字节记录整个压缩列表占用的内存字节数,包括zlbytes的四字节:在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。
zltailunit32_t4字节记录压缩列表表尾节点距离压缩列表的起始地址有多少字节: 通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址。
zlenunit16_t2字节记录了压缩列表包含的节点数量: 当这个属性的值小于 UINT16_MAX (65535)时, 这个属性的值就是压缩列表包含节点的数量; 当这个值等于 UINT16_MAX 时, 节点的真实数量需要遍历整个压缩列表才能计算得出。
entryX列表节点不定压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
zlendunit8_t1字节特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。

压缩列表组成示意图如下:
在这里插入图片描述

压缩列表节点的构成

压缩列表中有五个组成部分,其中最终要当属实际存储数据的zlentry节点,接下来将通过源码分析压缩列表节点的组成。

压缩列表节点结构定义

与压缩列表本身不同,节点具有明确的结构定义,压缩列表的节点在ziplist.c中定义如下:

typedef struct zlentry {
	// 用于记录内存编码后前一个entry的len占用了多少字节 即prevrawlen占用了多少字节
    unsigned int prevrawlensize; 
    // 用于记录前一个entry占用的长度 
    // 通过当前entry地址 - prevrawlen可以寻找到上一个entry的首地址
    unsigned int prevrawlen;     
    // 用于记录内存编码后当前entry的len占用了多少字节
    unsigned int lensize;        
    // 用于记录当前entry的长度
    unsigned int len;
    // 用于记录当前entry的header大小 即lensize + prevrawsize  
    unsigned int headersize;     
    // 用于记录当前节点编码格式
    unsigned char encoding;
    // 指向当前节点的指针      
    unsigned char *p;       
} zlentry;

prevrawlen的编码

zlentry中的prevrawlen长度并不是固定的,在prevrawlen的处理上使用了压缩编码,相关函数如下:

#define ZIP_BIG_PREVLEN 254

/* 对prevrawlen进行编码 或者直接返回编码len长度所需要的字节数 */
unsigned int zipStorePrevEntryLength(unsigned char *p, unsigned int len) {
	// 当传入p为NULL时 自然不需要编码 直接返回编码len长度所需要的字节数
    if (p == NULL) {
        return (len < ZIP_BIG_PREVLEN) ? 1 : sizeof(len)+1;
    } else {
    	// 如果len小于254 则直接使用一个字节保存
        if (len < ZIP_BIG_PREVLEN) {
            p[0] = len;
            return 1;
        // len大于等于254时调用函数处理
        } else {
            return zipStorePrevEntryLengthLarge(p,len);
        }
    }
}

int zipStorePrevEntryLengthLarge(unsigned char *p, unsigned int len) {
    if (p != NULL) {
    	// 首先首位固定设为254 表示节点长度一定大于等于254字节
        p[0] = ZIP_BIG_PREVLEN;
        // 随后开辟所需内存空间
        memcpy(p+1,&len,sizeof(len));
        // 大小端转换
        memrev32ifbe(p+1);
    }
    // p为NULL时直接返回编码len所需字节数
    return 1+sizeof(len);
}

从以上两个函数中可以发现,prevrawlen的编码有两种可能,即属性长度只可能是1字节或5字节

  • 当前一个节点长度小于254字节时,直接将前一节点存储在当前字节中
  • 当前一个节点长度大于等于254字节时,将prevrawlen的第一个字节设为254,后四个字节用于保存前一节点长度(因为unsigned int固定4字节)。

len与encoding编码

源码阅读到现在的部分,仍有一个重要的问题没用理清,就是zlentry能存放什么数据,这里先给出结论,zlentry被设置允许存放字符串和整数类型数据,由于压缩列表的主要目的是尽量节省空间,所以字符串和整数类型的数据有也自然有他的压缩(编码)方式。

字符串编码

#define ZIP_STR_MASK 0xc0
#define ZIP_INT_MASK 0x30

#define ZIP_IS_STR(enc) (((enc) & ZIP_STR_MASK) < ZIP_STR_MASK)

#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)

/* 与prevrawlen编码相同 p不为NULL时写入 为NULL时直接返回长度 */
unsigned int zipStoreEntryEncoding(unsigned char *p, unsigned char encoding, unsigned int rawlen) {
    unsigned char len = 1, buf[5];
	// 判断存放的是否是字符串
    if (ZIP_IS_STR(encoding)) {
    	// 长度 <= 0x3f 即63
        if (rawlen <= 0x3f) {
        	// 此处len不变 即只占位1字节
            if (!p) return len;
            // 该字节高位存放标识ZIP_STR_06B 0 << 6 即该字节高二位是00
            // 后6位实际存放长度 故表示能够存放63个字节
            buf[0] = ZIP_STR_06B | rawlen;
        // 长度 <= 0x3fff 即16383
        } else if (rawlen <= 0x3fff) {
        	// len扩大 占位2字节
            len += 1;
            if (!p) return len;
            // 首位字节高二位为ZIP_STR_14B 1 << 6 即高二位01
            // 其余14位实际存放长度 故表示能存放16383个字节
            buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
            buf[1] = rawlen & 0xff;
        // 其余情况 实际上是 长度小于4294967295
        } else {
        	// len扩大 占位5字节
            len += 4;
            if (!p) return len;
            // 首位字节为ZIP_STR_32B 2 << 6 高二位固定10 该字节其余位不使用
            // 剩余四个字节用于存放长度 故表示能存放4294967295个字节
            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 {
        // 不是字符串 那么编码编码为整型 长度固定位1 见整数编码
        if (!p) return len;
        buf[0] = encoding;
    }

    // p中存放
    memcpy(p,buf,len);
    return len;
}

从字符串编码的函数中可以明显的发现,在存放字符串(字节数组)时,有三种可能的长度,下附字节数组编码表格,其中x表示实际存放长度的位,_表示弃用不适用的位。

编码编码长度实际存储的数据
00xxxxxx1字节长度小于等于63字节的字节数组
01xxxxxx xxxxxxxx2字节长度小于等于16383字节的字节数组
10_ _ _ _ _ _ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx5字节长度小于4294 967 295字节的字节数组

整数编码

// 观察可见整数编码的高二位固定11 0xc0 = 1100 0000
#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

int zipTryEncoding(unsigned char *entry, unsigned int entrylen, long long *v, unsigned char *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. */
		// 0~12的整数值特殊 直接保存在encoding字段中
        if (value >= 0 && value <= 12) {
            *encoding = ZIP_INT_IMM_MIN+value;
        // 其余情况直接根据整数值范围 为encoding对应编码
        } 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;
}

从整型编码函数中可见,在存放整数时,有六种可能的长度,附整数编码表格如下:

编码编码长度实际存储的数据
110000001字节int16_t类型的整数
110100001字节int32_t类型的整数
111000001字节int64_t类型的整数
111100001字节24位有符号整数
111111101字节8位有符好整数
1111xxxx1字节encoding的低四位直接保存一个0~12的整数

ziplist的添加元素、查找元素、删除元素以及连锁更新的实现源码分析见第二篇博客。

部分理论内容和图片参考/摘自:《Redis设计与实现》——黄健宏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

7rulyL1ar

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

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

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

打赏作者

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

抵扣说明:

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

余额充值