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