39-源码 6:破旧立新 —— 探索「紧凑列表」内部
课程
1
开篇:授人以鱼不若授人以渔 —— Redis 可以用来做什么?
学习时长: 5分21秒
2
基础:万丈高楼平地起 —— Redis 基础数据结构
上次学到
学习时长: 16分14秒
3
应用 1:千帆竞发 —— 分布式锁
学习时长: 7分47秒
4
应用 2:缓兵之计 —— 延时队列
学习时长: 8分9秒
5
应用 3:节衣缩食 —— 位图
学习时长: 8分52秒
6
应用 4:四两拨千斤 —— HyperLogLog
学习时长: 14分17秒
7
应用 5:层峦叠嶂 —— 布隆过滤器
学习时长: 17分54秒
8
应用 6:断尾求生 —— 简单限流
学习时长: 4分37秒
9
应用 7:一毛不拔 —— 漏斗限流
学习时长: 7分22秒
10
应用 8:近水楼台 —— GeoHash
学习时长: 7分52秒
11
应用 9:大海捞针 —— Scan
学习时长: 8分42秒
12
原理 1:鞭辟入里 —— 线程 IO 模型
学习时长: 4分1秒
13
原理 2:交头接耳 —— 通信协议
学习时长: 3分34秒
14
原理 3:未雨绸缪 —— 持久化
学习时长: 5分27秒
15
原理 4:雷厉风行 —— 管道
学习时长: 3分51秒
16
原理 5:同舟共济 —— 事务
学习时长: 6分36秒
17
原理 6:小道消息 —— PubSub
学习时长: 7分7秒
18
原理 7:开源节流 —— 小对象压缩
学习时长: 7分14秒
19
原理 8:有备无患 —— 主从同步
学习时长: 4分9秒
20
集群 1:李代桃僵 —— Sentinel
学习时长: 3分52秒
21
集群 2:分而治之 —— Codis
学习时长: 7分28秒
22
集群 3:众志成城 —— Cluster
学习时长: 8分38秒
23
拓展 1:耳听八方 —— Stream
学习时长: 13分40秒
24
拓展 2:无所不知 —— Info 指令
学习时长: 4分4秒
25
拓展 3:拾遗补漏 —— 再谈分布式锁
学习时长: 2分18秒
26
拓展 4:朝生暮死 —— 过期策略
学习时长: 2分21秒
27
拓展 5:优胜劣汰 —— LRU
学习时长: 4分34秒
28
拓展 6:平波缓进 —— 懒惰删除
学习时长: 2分13秒
29
拓展 7:妙手仁心 —— 优雅地使用 Jedis
学习时长: 6分35秒
30
拓展 8:居安思危 —— 保护 Redis
学习时长: 2分19秒
31
拓展 9:隔墙有耳 —— Redis 安全通信
学习时长: 6分34秒
32
拓展 10:法力无边 —— Redis Lua 脚本执行原理
学习时长: 9分24秒
33
拓展 11:短小精悍 —— 命令行工具的妙用
学习时长: 9分21秒
34
源码 1:丝分缕析 —— 探索「字符串」内部
学习时长: 5分20秒
35
源码 2:循序渐进 —— 探索「字典」内部
学习时长: 7分24秒
36
源码 3:挨肩迭背 —— 探索「压缩列表」内部
学习时长: 10分42秒
37
源码 4:风驰电掣 —— 探索「快速列表」内部
学习时长: 3分49秒
38
源码 5:凌波微步 —— 探索「跳跃列表」内部
学习时长: 9分57秒
39
源码 6:破旧立新 —— 探索「紧凑列表」内部
学习时长: 2分42秒
40
源码 7:金枝玉叶 —— 探索「基数树」内部
学习时长: 5分36秒
41
源码 8:精益求精 —— LFU vs LRU
学习时长: 8分4秒
42
源码 9:如履薄冰 —— 懒惰删除的巨大牺牲
学习时长: 9分53秒
43
源码 10:跋山涉水 —— 深入字典遍历
学习时长: 9分24秒
44
源码 11:见缝插针 —— 探索 HyperLogLog 内部
学习时长: 13分3秒
45
尾声:百尺竿头 —— 继续深造指南
学习时长: 2分32秒
juejin_logo copyCreated with Sketch.

源码 6:破旧立新 —— 探索「紧凑列表」内部

Redis 5.0 又引入了一个新的数据结构 listpack,它是对 ziplist 结构的改进,在存储空间上会更加节省,而且结构上也比 ziplist 要精简。它的整体形式和 ziplist 还是比较接近的,如果你认真阅读了 ziplist 的内部结构分析,那么 listpack 也是比较容易理解的。

struct listpack<T> {
    int32 total_bytes; // 占用的总字节数
    int16 size; // 元素个数
    T[] entries; // 紧凑排列的元素列表
    int8 end; // 同 zlend 一样,恒为 0xFF
}

首先这个 listpack 跟 ziplist 的结构几乎一摸一样,只是少了一个zltail_offset字段。ziplist 通过这个字段来定位出最后一个元素的位置,用于逆序遍历。不过 listpack 可以通过其它方式来定位出最后一个元素的位置,所以zltail_offset字段就省掉了。

struct lpentry {
    int<var> encoding;
    optional byte[] content;
    int<var> length;
}

元素的结构和 ziplist 的元素结构也很类似,都是包含三个字段。不同的是长度字段放在了元素的尾部,而且存储的不是上一个元素的长度,是当前元素的长度。正是因为长度放在了尾部,所以可以省去了zltail_offset字段来标记最后一个元素的位置,这个位置可以通过total_bytes字段和最后一个元素的长度字段计算出来。

长度字段使用 varint 进行编码,不同于 skiplist 元素长度的编码为 1 个字节或者 5 个字节,listpack 元素长度的编码可以是 1、2、3、4、5 个字节。同 UTF8 编码一样,它通过字节的最高为是否为 1 来决定编码的长度。

同样,Redis 为了让 listpack 元素支持很多类型,它对 encoding 字段也进行了较为复杂的设计。

  1. 0xxxxxxx 表示非负小整数,可以表示0~127
  2. 10xxxxxx 表示小字符串,长度范围是0~63content字段为字符串的内容。
  3. 110xxxxx yyyyyyyy 表示有符号整数,范围是-2048~2047
  4. 1110xxxx yyyyyyyy 表示中等长度的字符串,长度范围是0~4095content字段为字符串的内容。
  5. 11110000 aaaaaaaa bbbbbbbb cccccccc dddddddd 表示大字符串,四个字节表示长度,content字段为字符串内容。
  6. 11110001 aaaaaaaa bbbbbbbb 表示 2 字节有符号整数。
  7. 11110010 aaaaaaaa bbbbbbbb cccccccc 表示 3 字节有符号整数。
  8. 11110011 aaaaaaaa bbbbbbbb cccccccc dddddddd 表示 4 字节有符号整数。
  9. 11110011 aaaaaaaa ... hhhhhhhh 表示 8 字节有符号整数。
  10. 11111111 表示 listpack 的结束符号,也就是0xFF

级联更新

listpack 的设计彻底消灭了 ziplist 存在的级联更新行为,元素与元素之间完全独立,不会因为一个元素的长度变长就导致后续的元素内容会受到影响。

取代 ziplist

listpack 的设计的目的是用来取代 ziplist,不过当下还没有做好替换 ziplist 的准备,因为有很多兼容性的问题需要考虑,ziplist 在 Redis 数据结构中使用太广泛了,替换起来复杂度会非常之高。它目前只使用在了新增加的 Stream 数据结构中。

思考

为什么 listpack 比 ziplist 更加优秀?

留言
Ctrl + Enter
全部评论(20)
BinK_1783的头像
删除
步履一双,踏遍山河,心有明月,山河明媚
点赞
回复
华北平原的头像
删除
length对解析内容没有影响,content的长度是凭借encoding解析出来的,length的作用,是为了反向遍历,这么理解对吗?
点赞
回复
华北平原的头像
删除
“长度字段使用 varint 进行编码,不同于 skiplist 元素长度的编码为 1 个字节或者 5 个字节”,这里应该是ziplist吧。
点赞
回复
春天百花开的头像
删除
为什么 listpack 比 ziplist 更加优秀?
1.无级联更新影响。首先消除了ziplist中级联更新的影响,每个元素相互独立,没有去记录对方的信息
2.简单的反向遍历方式。listpack列表通过其他方式实现了反向遍历,但需要依赖于元素的特性。而ziplist需要一直维护tail的偏移量
3.占用空间少。listpack的基础结构的占用空间比ziplist小(少了4byte,ziplist为11byte)
10
回复
秋泽的头像
删除
后端工程师 @ 字节跳动
消除级联更新,是说length不包含是指当前元素的长度,而不是上一个元素长度吧?
点赞
回复
TiTi57868的头像
删除
感觉ziplist和listpack做正向遍历都挺麻烦的啊 都需要从头指针开始 先是逐个解析total_bytes,tail_offset(对于ziplist来说),length这几个固定长度的字段,接着还要解析每个entry的encoding进而算出下一个元素的头需要往后跳过多少字节,然后才能解析出每个元素的内容啊 不知道理解得对不对?
点赞
回复
小左1470238168235的头像
删除
这种方式顺序怎么遍历呢?content跟length字段怎么区分出来
点赞
1
删除
明白了,通过encoding来区分
点赞
回复
梅小西爱学习的头像
删除
Java研发攻城狮 @ 某东南亚电商公司
可以省去了zltail_offset字段来标记最后一个元素的位置,这个位置可以通过total_bytes字段和最后一个元素的长度字段计算出来
---
这个描述有点没看懂?要定位最后一个元素的位置,还得依赖最后一个元素的长度字段,这个感觉有点死循环的意思啊?最后一个元素都没有找到,怎么去获得最后一个元素的长度呢?
1
8
删除
我的理解是:在作者提供的第一个代码片段中,其实是能获取listpack中的最后一个entry的(如果没有理解错的话),> T[] entries; // 紧凑排列的元素列表 < 。这样就可以通过total_bytes字段和最后一个元素的长度字段计算出来。
点赞
回复
删除
同问,还有怎么消除级联更新的?
点赞
回复
查看更多回复