簡介
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根據情況變化
插入就講到這里
代碼就不講了,其實主要就是一些內存申請、位移操作,比較繁瑣,但也不難