listpack如何遍历?如何解决ziplist连锁更新问题?(五)

listpack如何遍历?,如何解决ziplist连锁更新问题?(五)

继续上结的问题,为什么listpack 列表项只要记录当前项的长度,而解决了ziplist的问题呢?我们回顾一下ziplist问题来源。

前面我们提到过ziplist压缩列表的prevlen属性,为什么需要这个属性呢?它记录了「前一个节点」的长度其实是为了支持反向查询列表。

但是prevlen 是根据数据的大小来进行不同的空间大小分配, prevlen 属性的空间大小跟前一个节点长度值有关,比如:

  • 如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值;
  • 如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值;

所以会容易导致内存重新分配,甚至可能引发连锁更新的问题。

原来prevlen属性的问题解决了正反遍历,但会导致内存重新分配。listpack 列表项只记录当前项的长度,怎么解决正反遍历问题呢?

回顾listpack 结构:

在这里插入图片描述

  • encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
  • data,实际存放的数据;
  • len,encoding+data的总长度;

当应用程序从左向右正向查询 listpack 时,我们可以先调用 lpFirst 函数。该函数的参数是指向 listpack 头的指针,它在执行时,会让指针向右偏移 LP_HDR_SIZE 大小,也就是跳过 listpack 头。你可以看下 lpFirst 函数的代码,如下所示:

unsigned char *lpFirst(unsigned char *lp) { 

lp += LP_HDR_SIZE; //跳过listpack头部6个字节

if (lp[0] == LP_EOF) return NULL; //如果已经是listpack的末尾结束字节,则返回NU 

return lp; 

} 

然后,再调用 lpNext 函数,该函数的参数包括了指向 listpack 某个列表项的指针。lpNext 函数会进一步调用 lpSkip 函数,并传入当前列表项的指针,如下所示:

unsigned char *lpNext(unsigned char *lp, unsigned char *p) { 

... 

p = lpSkip(p); //调用lpSkip函数,偏移指针指向下一个列表项

if (p[0] == LP_EOF) return NULL; 

return p; 

}

最后,lpSkip 函数会先后调用 lpCurrentEncodedSize 和 lpEncodeBacklen 这两个函数。lpCurrentEncodedSize 函数是根据当前列表项第 1 个字节的取值,来计算当前项的编码类型,并根据编码类型,计算当前项编码类型和实际数据的总长度。然后,lpEncodeBacklen 函数会根据编码类型和实际数据的长度之和,进一步计算列表项最后一部分 entry-len 本身的长度。

这样一来,lpSkip 函数就知道当前项的编码类型、实际数据和 entry-len 的总长度了,也就可以将当前项指针向右偏移相应的长度,从而实现查到下一个列表项的目的。

下面代码展示了 lpEncodeBacklen 函数的基本计算逻辑,你可以看下。

unsigned long lpEncodeBacklen(unsigned char *buf, uint64_t l) {
	//编码类型和实际数据的总长度小于等于127,entry-len长度为1字节
	if (l <= 127) {
		... 
		return 1;
	} else if (l < 16383) {
		//编码类型和实际数据的总长度大于127但小于16383,entry-len 
		...
		return 2;
	} else if (l < 2097151) {
		//编码类型和实际数据的总长度大于16383但小于2097151,ent 
		...
		return 3;
	} else if (l < 268435455) {
		//编码类型和实际数据的总长度大于2097151但小于2684354 
		... 
		return 4;
	} else {
		//否则,entry-len长度为5字节
		..
		return 5;
	}
}

我也画了一张图,展示了从左向右遍历 listpack 的基本过程,你可以再回顾下。

在这里插入图片描述

好,了解了从左向右正向查询 listpack,我们再来看下从右向左反向查询 listpack

首先,我们根据 listpack 头中记录的 listpack 总长度,就可以直接定位到 listapck 的尾部结束标记。然后,我们可以调用 lpPrev 函数,该函数的参数包括指向某个列表项的指针,并返回指向当前列表项前一项的指针。

lpPrev 函数中的关键一步就是调用 lpDecodeBacklen 函数。lpDecodeBacklen 函数会从右向左,逐个字节地读取当前列表项的 entry-len。

那么,lpDecodeBacklen 函数如何判断 entry-len 是否结束了呢?

这就依赖于 entry-len 的编码方式了。entry-len 每个字节的最高位,是用来表示当前字节是否为 entry-len 的最后一个字节,这里存在两种情况,分别是:

  • 最高位为 1,表示 entry-len 还没有结束,当前字节的左边字节仍然表示 entry-len 的内容;
  • 最高位为 0,表示当前字节已经是 entry-len 最后一个字节了。

而 entry-len 每个字节的低 7 位,则记录了实际的长度信息。这里你需要注意的是,entry-len 每个字节的低 7 位采用了大端模式存储,也就是说,entry-len 的低位字节保存在内存高地址上。

我画了下面这张图,展示了 entry-len 这种特别的编码方式,你可以看下。

在这里插入图片描述

实际上,正是因为有了 entry-len 的特别编码方式,lpDecodeBacklen 函数就可以从当前列表项起始位置的指针开始,向左逐个字节解析,得到前一项的 entry-len 值。这也是lpDecodeBacklen 函数的返回值。而从刚才的介绍中,我们知道 entry-len 记录了编码类型和实际数据的长度之和。

因此,lpPrev 函数会再调用 lpEncodeBacklen 函数,来计算得到 entry-len 本身长度,这样一来,我们就可以得到前一项的总长度,而 lpPrev 函数也就可以将指针指向前一项的起始位置了。所以按照这个方法,listpack 就实现了从右向左的查询功能。

小结:解决连锁更新

在 listpack 中,因为每个列表项只记录自己的长度,而不会像 ziplist 中的列表项那样,会记录前一项的长度。所以,当我们在 listpack 中新增或修改元素时,实际上只会涉及每个列表项自己的操作,而不会影响后续列表项的长度变化,这就避免了连锁更新。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值