40-源码 7:金枝玉叶 —— 探索「基数树」内部
课程
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.

源码 7:金枝玉叶 —— 探索「基数树」内部

Rax 是 Redis 内部比较特殊的一个数据结构,它是一个有序字典树 (基数树 Radix Tree),按照 key 的字典序排列,支持快速地定位、插入和删除操作。Redis 五大基础数据结构里面,能作为字典使用的有 hash 和 zset。hash 不具备排序功能,zset 则是按照 score 进行排序的。rax 跟 zset 的不同在于它是按照 key 进行排序的。Redis 作者认为 rax 的结构非常易于理解,但是实现却有相当的复杂度,需要考虑很多的边界条件,需要处理节点的分裂、合并,一不小心就会出错。

应用

你可以将一本英语字典看成一棵 radix tree,它所有的单词都是按照字典序进行排列,每个词汇都会附带一个解释,这个解释就是 key 对应的 value。有了这棵树,你就可以快速地检索单词,还可以查询以某个前缀开头的单词有哪些。

你也可以将公安局的人员档案信息看成一棵 radix tree,它的 key 是每个人的身份证号,value 是这个人的履历。因为身份证号的编码的前缀是按照地区进行一级一级划分的,这点和单词非常类似。有了这棵树,你就可以快速地定位出人员档案,还可以快速查询出某个小片区都有哪些人。

Radix tree 还可以应用于时间序列应用,key 为时间戳,value 为发生在具体时间的事件内容。因为时间戳的编码也是按照【年月日时分秒毫秒微秒纳秒】进行一级一级划分的,所以它也可以使用字典序来排序。有了这棵数,我们就可以快速定位出某个具体时间发生了什么事,也可以查询出一段时间内都有哪些事发生。

我们经常使用的 Web 服务器的 Router 它也是一棵 radix tree。这棵树上挂满了 URL 规则,每个 URL 规则上都会附上一个请求处理器。当一个请求到来时,我们拿这个请求的 URL 沿着树进行遍历,找到相应的请求处理器来处理。因为 URL 中可能存在正则 pattern,而且同一层的节点对顺序没有要求,所以它不算是一棵严格的 radix tree。

# golang 的 HttpRouter 库
The router relies on a tree structure which makes heavy use of *common prefixes*
it is basically a *compact* [*prefix tree*](https://en.wikipedia.org/wiki/Trie) 
(or just [*Radix tree*](https://en.wikipedia.org/wiki/Radix_tree)). 
Nodes with a common prefix also share a common parent. 
Here is a short example what the routing tree for the `GET` request method could look like:

Priority   Path             Handle
9          \                *<1>
3          ├s               nil
2          |├earch\         *<2>
1          |└upport\        *<3>
2          ├blog\           *<4>
1          |    └:post      nil
1          |         └\     *<5>
2          ├about-us\       *<6>
1          |        └team\  *<7>
1          └contact\        *<8>

Rax 被用在 Redis Stream 结构里面用于存储消息队列,在 Stream 里面消息 ID 的前缀是时间戳 + 序号,这样的消息可以理解为时间序列消息。使用 Rax 结构进行存储就可以快速地根据消息 ID 定位到具体的消息,然后继续遍历指定消息之后的所有消息。

Rax 被用在 Redis Cluster 中用来记录槽位和key的对应关系,这个对应关系的变量名成叫着slots_to_keys。这个raxNode的key是由槽位编号hashslot和key组合而成的。我们知道cluster的槽位数量是16384,它需要2个字节来表示,所以rax节点里存的key就是2个字节的hashslot和对象key拼接起来的字符串。

因为rax的key是按照key前缀顺序挂接的,意味着同样的hashslot的对象key将会挂在同一个raxNode下面。这样我们就可以快速遍历具体某个槽位下面的所有对象key。

结构

rax 中有非常多的节点,根节点、叶节点和中间节点,有些中间节点带有 value,有些中间节点纯粹是结构性需要没有对应的 value。

struct raxNode {
    int<1> isKey; // 是否没有 key,没有 key 的是根节点
    int<1> isNull; // 是否没有对应的 value,无意义的中间节点
    int<1> isCompressed; // 是否压缩存储,这个压缩的概念比较特别
    int<29> size; // 子节点的数量或者是压缩字符串的长度 (isCompressed)
    byte[] data; // 路由键、子节点指针、value 都在这里
}

rax 是一棵比较特殊的 radix tree,它在结构上不是标准的 radix tree。如果一个中间节点有多个子节点,那么路由键就只是一个字符。如果只有一个子节点,那么路由键就是一个字符串。后者就是所谓的「压缩」形式,多个字符压在一起的字符串。比如前面的那棵字典树在 Rax 算法中将呈现出如下结构:

图中的深蓝色节点就是「压缩」节点。

接下来我们再细看raxNode.data里面存储的到底是什么东西,它是一个比较复杂的结构,按照压缩与否分为两种结构

压缩结构 子节点如果只有一个,那就是压缩结构,data 字段如下伪代码所示:

struct data {
    optional struct { // 取决于 header 的 size 字段是否为零
        byte[] childKey; // 路由键
        raxNode* childNode; // 子节点指针
    } child;
    optional string value; // 取决于 header 的 isNull 字段
}

如果是叶子节点,child 字段就不存在。如果是无意义的中间节点 (isNull),那么 value 字段就不存在。

非压缩节点 如果子节点有多个,那就不是压缩结构,存在多个路由键,一个键是一个字符。

struct data {
    byte[] childKeys; // 路由键字符列表
    raxNode*[] childNodes; // 多个子节点指针
    optional string value; // 取决于 header 的 isNull 字段
}

也许你会想到如果子节点只有一个,并且路由键字符串的长度为 1 呢,那到底算压缩还是非压缩?仔细思考一下,在这种情况下,压缩和非压缩在数据结构表现形式上是一样的,不管 isCompressed 是 0 还好是 1,结构都是一样的。

增删节点

Rax 的增删节点逻辑非常复杂,代码里充斥了太多ifelse逻辑,老师我看的是晕头转向,所以这里也就不分析它的源码了,以后要是彻底看懂了,再来继续跟同学们分享吧。如果哪位同学想挑战一下,可以试试看!

思考

还有哪些场合可以使用 radix tree?

留言
Ctrl + Enter
全部评论(17)
柏油的头像
删除
JAVA后端
前缀数,其目的是针对相同前缀实现共用,从而节省空间,同时还能快速遍历,blog.csdn.net 前缀树深入分析,大家可以参考下。
点赞
回复
BinK_1783的头像
删除
愿你霜尘梦不朽,也有白月牵衣袖,也有春秋抚眉头
点赞
回复
文艺的小逗比的头像
删除
httprouter 对 每个节点的 key 的首字母进行了排序的。
点赞
回复
怪兽的实验室的头像
删除
你这些图都是用什么画的呀?
点赞
1
删除
processon
点赞
回复
liliqiu102的头像
删除
go后端研发
这节没看懂。。。
点赞
回复
_爱学习51557的头像
删除
有些中间节点纯粹是结构性需要没有对应的 value。这句话打错了么,老钱使用的是五笔么
点赞
回复
xxsh的头像
删除
和java中的treeMap很像?
点赞
回复
Yun_wg的头像
删除
跟trie tree 有什么区别
2
1
删除
看起来是同一个东西,不同的名字而已
点赞
回复
删除
回复
区别在于这里的一个节点可以装多个字符,在trie tree里面则每个节点只能装一个;
详细解释请见:
stackoverflow.com
点赞
回复
LeGrandmechantRenard的头像
删除
想吃软饭 @ 某支付公司
说实话 老师,我看到36 我都开始完全蒙了。能坚持到现在 很不错了。自我鼓励一下。
4
3
删除
(作者)
源码真是极度深寒啊
点赞
回复
删除
跟 trie tree 有什么区别
点赞
回复
查看更多回复