<贰>redis源码分析之压缩列表(ziplist)

压缩列表(ziplist)是哈希键的底层实现之一。它是经过特殊编码的双向链表,和整数集合(intset)一样,是为了提高内存的存储效率而设计的。

Redis 压缩列表(ziplist)

ziplist数据结构

redis中ziplist是由ziplist header 、entries、zlend三个部分组成,在内存中的布局如下所示:
在这里插入图片描述

头部结构

由zlbytes、zltail、zlen三个部分组成:

zlbytes:压缩列表总字节数
zltail:压缩列表头尾偏移量
zlen:压缩列表节点数量

// 10个字节
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))

entries结构

prev_entry_length:编码前置节点的长度,用于从后往前遍历
encoding:编码属性
contents:负责保存节点的值

prev_entry_length:

ziplist在编码前置节点长度的时候,采用以下规则:

如果前置节点的长度小于254字节,那么采用1个字节来保存这个长度值
如果前置节点的长度大于254字节,则采用5个字节来保存这个长度值,其中,第一个字节被设置为0xFE(254),用于表示该长度大于254字节,后面四个字节则用来存储前置节点的长度值。

encoding:

ziplist的节点可以保存字符串值和整数值

节点保存字符串值

1.如果节点保存的是字符串值,那么该编码大小可能为1字节,2字节或5字节,这与字符串的长度有关。编码部分前两位为00,01或者10,分别对应上述的三种大小,后面的位表示长度大小值。

2.字符串大小编码长度编码<= 63 bytes1 bytes00bbbbbb<= 16383 bytes2 bytes01bbbbbb xxxxxxxx<= 4294967295 bytes5 bytes10________ aaaaaaaa bbbbbbbb cccccccc dddddddd

节点保存整数值

1.如果节点保存的是整数值,那么其编码长度固定为1个字节,该字节的前两位固定为11,用于表示节点保存的是整数值。这里也用一个表来说明。

2.整数类型编码长度编码:
int16_t(2 bytes)111000000
int32_t(4 bytes)111010000
int64_t(8bytes) 111100000
24位有符整数 111110000
8位有符整数 1111111100~1211111xxxx

encoding与计算encoding类型->

//字符串
#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)

创建节点

/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
    unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
    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;
}

插入节点

static 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;
	//初始化,防止报错
    PORT_LONGLONG value = 123456789; 
    zlentry tail;

	// 找出待插入节点的前置节点长度
	// 如果p[0]不指向列表末端,说明列表非空,并且p指向其中一个节点
    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);
        }
    }

    //判断是否能够编码为整数
    if (zipTryEncoding(s,slen,&value,&encoding)) {
        //编码为整数的长度
        reqlen = zipIntSize(encoding);
    } else {
        //编码为字符串的长度
        reqlen = slen;
    }
    //获取前置节点的长度
    reqlen += zipPrevEncodeLength(NULL,(unsigned int)prevlen);                  WIN_PORT_FIX /* cast (unsigned int) */
	//当前节点的长度
    reqlen += zipEncodeLength(NULL,encoding,slen);

	/* 只要不是插入到列表的末端,都需要判断当前p所指向的节点header是否能存放新节点的长度编码
	 * nextdiff保存新旧编码之间的字节大小差,如果这个值大于0
	 * 那就说明当前p指向的节点的header进行扩展*/
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,(unsigned int)reqlen) : 0; WIN_PORT_FIX /* cast (unsigned int) */

    /* 存储p相遇对列表zl的偏移地址. */
    offset = p-zl;
	/* 重新分配空间,curlen当前列表的长度
	 * reqlen 新节点的全部长度
	 * nextdiff 新节点的后继节点扩展header的长度*/
    zl = ziplistResize(zl,(unsigned int)(curlen+reqlen+nextdiff));              WIN_PORT_FIX /* cast (unsigned int) */
	// 重新获取p的值
    p = zl+offset;

	// 非表尾插入,需要重新计算表尾的偏移量
    if (p[0] != ZIP_END) {
		// 移动现有元素,为新元素的插入提供空间
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);

		// p+reqlen为新节点前置节点移动后的位置,将新节点的长度编码至前置节点
        zipPrevEncodeLength(p+reqlen,(unsigned int)reqlen);                     WIN_PORT_FIX /* cast (unsigned int) */

		// 更新列表尾相对于表头的偏移量,将新节点的长度算上
        ZIPLIST_TAIL_OFFSET(zl) =
            (uint32_t)intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen); WIN_PORT_FIX /* cast (uint32_t) */

		// 如果新节点后面有多个节点,那么表尾的偏移量需要算上nextdiff的值
        zipEntry(p+reqlen, &tail);
        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) = (uint32_t)intrev32ifbe(p-zl);                 WIN_PORT_FIX /* cast (uint32_) */
    }

	// 当nextdiff不为0时,表示需要新节点的后继节点对头部进行扩展
    if (nextdiff != 0) {
        offset = p-zl;
		// 需要对p所指向的机电header进行扩展更新
	    // 有可能会引起连锁更新
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }

	// 将新节点前置节点的长度写入新节点的header
    p += zipPrevEncodeLength(p,(unsigned int)prevlen);                          WIN_PORT_FIX /* cast (unsigned int) */
	// 将新节点的值长度写入新节点的header
    p += zipEncodeLength(p,encoding,slen);
	// 写入节点值
    if (ZIP_IS_STR(encoding)) {
        memcpy(p,s,slen);
    } else {
        zipSaveInteger(p,value,encoding);
    }
	// 更新列表节点计数
    ZIPLIST_INCR_LENGTH(zl,1);
    return zl;
}

插入节点的时候有可能会产生连锁更新,我们来看下连锁更新的定义:

当新节点插入后,需要改变新节点后继节点的header信息中的保存前置节点长度的部分,如果这个pre_entry_length原存放的长度小于254字节,也就是只用了一个字节,现在新节点的长度大于254字节,需要用5个字节保存,这样就要对这个pre_entry_length进行扩展。试想一下,如果扩展之后该节点的整体长度大于254字节了,那么该节点的后继节点是不是也需要更新header信息呢?答案是肯定的,这样就引发了连锁更新,导致新节点后面的一连串节点都需要对header进行扩容。

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) {
		// 将p所指向节点的信息保存到cur结构体中
        zipEntry(p, &cur);
		// 当前节点的长度
        rawlen = cur.headersize + cur.len;
		// 编码当前节点的长度所需的字节数
        rawlensize = zipPrevEncodeLength(NULL,(unsigned int)rawlen);            WIN_PORT_FIX /* cast (unsigned int) */

		// 如果没有后续节点需要更新了,就退出
        if (p[rawlen] == ZIP_END) break;
		// 去除后续节点的信息保存到next结构体中
        zipEntry(p+rawlen, &next);

		// 当后续节点的空间已经足够了,就直接退出
        if (next.prevrawlen == rawlen) break;
		// 当后续节点的空间不足够,则需要进行扩容操作
        if (next.prevrawlensize < rawlensize) {
			// 记录p的偏移值
            offset = p-zl;
			// 记录需要增加的长度
            extra = rawlensize-next.prevrawlensize;
			// 扩展zl的大小
            zl = ziplistResize(zl,(unsigned int)(curlen+extra));                WIN_PORT_FIX /* cast (unsigned int) */
			// 获取p相对于新的zl的值
            p = zl+offset;

			//记录下一个节点的偏移量
            np = p+rawlen;
            noffset = np-zl;

			// 当 next 节点不是表尾节点时,更新列表到表尾节点的偏移量
            if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
                ZIPLIST_TAIL_OFFSET(zl) =
                    (uint32_t)intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra); WIN_PORT_FIX /* cast (uint32_t) */
            }

			// 向后移动cur节点之后的数据,为新的header腾出空间
            memmove(np+rawlensize,
                np+next.prevrawlensize,
                curlen-noffset-next.prevrawlensize-1);
            zipPrevEncodeLength(np,(unsigned int)rawlen);                       WIN_PORT_FIX /* cast (unsigned int) */

			// 移动指针,继续处理下一个节点
            p += rawlen;
            curlen += extra;
        } else {
            if (next.prevrawlensize > rawlensize) {
				// 执行到这里,next节点编码前置节点的header空间有5个字节
				// 但是此时只需要一个字节
				// Redis不提供缩小操作,而是直接将长度强制性写入五个字节中
                zipPrevEncodeLengthForceLarge(p+rawlen,(unsigned int)rawlen);
            } else {
				// 运行到这里,说明刚好可以存放
                zipPrevEncodeLength(p+rawlen,(unsigned int)rawlen);
            }

			// 退出,代表空间足够,后续空间不需要更改
            break;
        }
    }
    return zl;
}

删除节点

/* Delete "num" entries, starting at "p". Returns pointer to the ziplist. */
static 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;
	// 获取p指向的节点信息
    zipEntry(p, &first);
	// 计算num个节点占用的内存
    for (i = 0; p[0] != ZIP_END && i < num; i++) {
        p += zipRawEntryLength(p);
        deleted++;
    }

    totlen = (unsigned int)(p-first.p);                                         WIN_PORT_FIX /* cast (unsigned int) */
    if (totlen > 0) {
        if (p[0] != ZIP_END) {
			// 执行到这里,表示被删除节点后面还存在节点
			// 判断最后一个被删除的节点的后继节点的header中的存放前置节点长度的空间
			// 能不能容纳第一个被删除节点的前置节点的长度
            nextdiff = zipPrevLenByteDiff(p,first.prevrawlen);
            p -= nextdiff;
            zipPrevEncodeLength(p,first.prevrawlen);

			// 更新尾部相对于头部的偏移量
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);

			// 如果被删除节点后面还存在节点,就需要将nextdiff计算在内
            zipEntry(p, &tail);
            if (p[tail.headersize+tail.len] != ZIP_END) {
                ZIPLIST_TAIL_OFFSET(zl) =
                   intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
            }

			// 将被删除节点后面的内存空间移动到删除的节点之后
            memmove(first.p,p,
                intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
        } else {
			// 执行到这里,表示被删除节点后面没有节点了
            ZIPLIST_TAIL_OFFSET(zl) =
                (unsigned int)intrev32ifbe((first.p-zl)-first.prevrawlen);      WIN_PORT_FIX /* cast (unsigned int) */
        }

		// 缩小内存并更新ziplist的长度
        offset = first.p-zl;
        zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
        ZIPLIST_INCR_LENGTH(zl,-deleted);
        p = zl+offset;

		// 如果nextdiff不等于0,说明被删除节点后面节点的header信息还需要更改,需要进行连锁更新
        if (nextdiff != 0)
            zl = __ziplistCascadeUpdate(zl,p);
    }
    return zl;
}

end结构

ziplist尾部的zlend则表示压缩列表结束,其值固定为0xFF,长度为1个字节。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

无痕Miss

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

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

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

打赏作者

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

抵扣说明:

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

余额充值