Redis(2)——数据结构之RAX


简介

RAX叫做基数树(前缀压缩树),就是有相同前缀的字符串,其前缀可以作为一个公共的父节点
redis源码中对应的是rax.c和rax.h
源码中的说明:

 * 假设要存三个字符串:foo, footer, foobar
 * 这是一个没有压缩的结构
 *
 *              (f) ""
 *                \
 *                (o) "f"
 *                  \
 *                  (o) "fo"
 *                    \
 *                  [t   b] "foo"
 *                  /     \
 *         "foot" (e)     (a) "foob"
 *                /         \
 *      "foote" (r)         (r) "fooba"
 *              /             \
 *    "footer" []             [] "foobar"

 *  我们进行一下压缩:
 *                  ["foo"] ""
 *                     |
 *                  [t   b] "foo"
 *                  /     \
 *        "foot" ("er")    ("ar") "foob"
 *                 /          \
 *       "footer" []          [] "foobar"
 * redis中基本就是这个样子

 * 如果我们再插入一个first:
 *
 *                    (f) ""
 *                    /
 *                 (i o) "f"
 *                 /   \
 *    "firs"  ("rst")  (o) "fo"
 *              /        \
 *    "first" []       [t   b] "foo"
 *                     /     \
 *           "foot" ("er")    ("ar") "foob"
 *                    /          \
 *          "footer" []          [] "foobar"
 * 那就变成了这个样子

基本了解之后,来看一下基本概念

结构

看一下,一个节点是什么样的

对应的结构体定义:

typedef struct raxNode {
    uint32_t iskey:1;     /* Does this node contain a key? */
    uint32_t isnull:1;    /* Associated value is NULL (don't store it). */
    uint32_t iscompr:1;   /* Node is compressed. */
    uint32_t size:29;     /* Number of children, or compressed string len. */
    
    unsigned char data[];
} raxNode;

一个node有5个部分

  • iskey:如果为1,那表示前面的字符串组成一个完整的Key
  • isnull:字符串是可以对应有一个value指针的,就像key-value的对应关系;这个value可以是NULL
  • iscompr:是否压缩节点
  • size: 该节点含有的字符串长度,注意,并不是data数组的长度

假设现在有一个节点存储了"abc"这3个字符:

看图,因为有abc三个字符,所以size是3

前面三个iskey, isnull, 先不管

iscompr为0,表示非压缩节点

data中绿色的padding,不存放任何数据,其意义在于让数组的大小=指针大小的倍数,内存取整
padding后面的三个指针就是abc这三个字符分别的后继
这3个指针也是raxNode指针

如果这个节点是rax树中的一个节点,那我们可以猜想rax含有:xxxayyyy, xxxbzzzzz, xxxcttttt 这3个key字符串
xxx是共同前缀,后面yyyy,zzzz,ttttt就是分别的后缀

那压缩节点呢?

iscompr为1
后面就只有一个raxNode指针

如果是压缩节点,那表示共同的前缀abc,就如同开头的例子中的"foo"

插入分析

直接看代码的话,其实挺迷的
因此,我们从实际例子出发
观察一个rax树,从新建到插入数据的变化过程

理解了这个过程,再看代码就会容易很多
(假设指针大小为4字节)

刚新建

rax结构体表示rax树
raxNode结构体表示rax树中的节点

typedef struct rax {
    raxNode *head;  //指向新的raxNode
    uint64_t numele; // 0   完整的key的数量
    uint64_t numnodes;  // 1  raxNode的数量
} rax;

rax->head指向下面这个raxNode

typedef struct raxNode {
    uint32_t iskey:1;     /* Does this node contain a key? */
    uint32_t isnull:1;    /* Associated value is NULL (don't store it). */
    uint32_t iscompr:1;   /* Node is compressed. */
    uint32_t size:29;     /* Number of children, or compressed string len. */
    
    unsigned char data[];
} raxNode;

全是0,data没有

刚新建的时候,只有一个空节点

插入abcdefg

rax是可以保存key->value的,key是字符串,value任意

因此需要区分有没有value,raxNode->isnull为1表示没有value,为0表示有

rax->head会变成:

typedef struct raxNode {
    uint32_t iskey       // 0
    uint32_t isnull      // 0
    uint32_t iscompr     // 1
    uint32_t size        // 7
    
    unsigned char data[];
    // |a|b|c|d|e|f|g| |ptr raxNode*|
    // 存入的数据需要分配的空间为指针大小(4字节)的倍数,所以会多一个字节
    // 最后是一个指针,指向下一个raxNode
    // 注意,size并不是data真正的长度
} raxNode;

可以看到,data中abcdefg这7个字符后面,还有1个字节的空白,这是为了凑够8字节,刚好是指针占用字节数的2倍

ptr raxNode*:

typedef struct raxNode {
    uint32_t iskey       // 1 为1表示前面的组成一个key
    uint32_t isnull      // 1 或 0
    uint32_t iscompr     // 0
    uint32_t size        // 0
    
    unsigned char data[];
    //如果没有value,那data没有;如果有,那data有一个指针的长度
} raxNode;

注意这个iskey,表示的是前面一串的节点组成一个完整的key,不包括当前节点

这时:
rax->numele=1, rax->numnodes=2


先看一下这个函数,这个函数把一个raxNode变为压缩节点

函数 raxCompressNode (raxNode *n, unsigned char *s, size_t len, raxNode **child):
参数:
child指针 新建Node raxNewNode

函数的执行内容:
n->data的大小变为:raxNode结构体长度+len数据长度+sizeof(raxNode*),指针因为是压缩节点后面只有一个指针

n->iscompr = 1

n->size = len

s的数据内容复制到n

n的最后的指针为child(刚才新建的Node)

这个函数把n变成压缩节点,s的数据覆盖到n节点;n的最后的指针指向新创建的Node,传入的child指向这个新创建的Node

n的iscompr和size属性发生了变化,iskey和isnull没变


h即rax->head,需要变成压缩节点(调用raxCompressNode)
parentlink = rax->head的最后那个指针
rax->numnodes++

h指向下一个raxNode

如果data有,把h扩大void*

rax->numele++

h->key = 1
如果data没有,h->isnull=1;如果有data,h的最后一个指针指向data

return 1


插入abcxyz

演变过程:

现在rax有2个node,我们把它们叫做node1 和 node2

观察发现,rax当前有字符串abcdefg,和要插入的abcxyz,前3个字符是相同的

因此,新建一个node3 放前3个字符abc

node3
typedef struct raxNode {
    uint32_t iskey       // 0
    uint32_t isnull      // 0
    uint32_t iscompr     // 1
    uint32_t size        // 3
    
    unsigned char data[];
    //内存对齐,虽然只有3个字符,实际占用了4个字节,最后有一个指针
    //指针指向node4
    // abc_ node*
} raxNode;

然后来到分界点,需要再新建一个node4

node4
typedef struct raxNode {
    uint32_t iskey       // 0
    uint32_t isnull      // 0
    uint32_t iscompr     // 0
    uint32_t size        // 2
    
    unsigned char data[];
    //内存对齐,有2个字符d和x(按照字符升序排列),实际占用了4个字节,最后有2个指针
    // dx__ node* node*
} raxNode;

因为这里分出了2条字符串,所以node4 有2个字符,作为2个引导字符,也有2个node指针,指向后续的字符串

先来看d这条分支,这代表的是原来的那个abcdefg,剩下的efg在下一个node,node5

node5
typedef struct raxNode {
    uint32_t iskey       // 0
    uint32_t isnull      // 0
    uint32_t iscompr     // 1
    uint32_t size        // 3
    
    unsigned char data[];
    //内存对齐,有3个字符efg,实际占用了4个字节,最后有1个指针
    // efg_ node* 
} raxNode;

最后的这个指针指向原来那个node2,node2:

node2
typedef struct raxNode {
    uint32_t iskey       // 1 为1表示前面的组成一个key
    uint32_t isnull      // 1 或 0
    uint32_t iscompr     // 0
    uint32_t size        // 0
    
    unsigned char data[];
    //如果没有value,那data没有;如果有,那data有一个指针的长度
} raxNode;

现在原来的abcdefg,被拆分成了3个node(abc、d、efg),加上原来的node1,一共4个node

回到分界点node4,第二条分支,以字符x 开始

新建node6,放yz

node6
typedef struct raxNode {
    uint32_t iskey       // 0
    uint32_t isnull      // 0
    uint32_t iscompr     // 1
    uint32_t size        // 2
    
    unsigned char data[];
    //内存对齐,有2个字符yz,实际占用了4个字节,最后有1个指针
    // yz__ node* 
} raxNode;

再新建一个node7,和node2一样,作为结尾;node6最后的指针指向node7

node7
typedef struct raxNode {
    uint32_t iskey       // 1 为1表示前面的组成一个key
    uint32_t isnull      // 1 或 0
    uint32_t iscompr     // 0
    uint32_t size        // 0
    
    unsigned char data[];
    //如果没有value,那data没有;如果有,那data有一个指针的长度
} raxNode;

最后,rax->head = node3

原来的node1 释放

插入完成,图示:

node3(abc) -> node4(dx) d的分支-> node5(efg) -> node2()

____________x的分支-> node6(yz) -> node7()

插入abcxyzwhat

把node7 放入what,node7变成压缩节点

然后新建节点node8,node8->iskey=1,作为终止节点,和node2一样

node3(abc) -> node4(dx) d的分支-> node5(efg) -> node2()

____________x的分支-> node6(yz) -> node7(what) -> node8()

插入 ab

由于ab在第一个节点abc中已经存在,因此需要将node3拆成2个node

新建node9,装ab,这个node会成为rax->head

node9
typedef struct raxNode {
    uint32_t iskey       // 为原来的node3->iskey
    uint32_t isnull      // 为原来的node3->isnull
    uint32_t iscompr     // 1
    uint32_t size        // 2
    
    unsigned char data[];
    //ab__ node* void*(取决于node3有没有data)
} raxNode;

然后新建node10,node9后面是node10

node10
typedef struct raxNode {
    uint32_t iskey       // 1,表明node9组成了key
    uint32_t isnull      // 取决于插入ab时,有没有data
    uint32_t iscompr     // 1
    uint32_t size        // 1
    
    unsigned char data[];
    //c___ node* void*(取决于node3有没有data)
    //node*指向node4,后面保持一致
} raxNode;

node3释放

插入后结构:

node9(ab) -> node10(c) -> node4(dx) d的分支-> node5(efg) -> node2()

__________________x的分支-> node6(yz) -> node7(what) -> node8()

插入abcx

看到node4已经有x 了,这时不需要新增节点

我们要做的是修改node6

node6原来的样子:

node6
typedef struct raxNode {
    uint32_t iskey       // 0
    uint32_t isnull      // 0
    uint32_t iscompr     // 1
    uint32_t size        // 2
    
    unsigned char data[];
    //内存对齐,有2个字符yz,实际占用了4个字节,最后有1个指针
    // yz__ node* 
} raxNode;

由于前面的组成key,node6->iskey=1,rax->numele++

如果data有,node6需要变大void*,isnull根据情况变化


插入就讲到这里
代码就不讲了,其实主要就是一些内存申请、位移操作,比较繁琐,但也不难


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM