Rax全稱redis tree,是一個有序字典樹,可以根據key進行排序,支持快速定位、插入與刪除,與hash/zset不同在於hash不具備排序功能,zset則根據score進行排序。
【trie簡介】
在計算機科學中,trie,又稱前綴樹或字典樹,是一種有序樹,用於保存關聯數組,其中的鍵通常是字符串。與二叉查找樹不同,鍵不是直接保存在節點中,而是由節點在樹中的位置決定。一個節點的所有子孫都有相同的前綴,也就是這個節點對應的字符串,而根節點對應空字符串。一般情況下,不是所有的節點都有對應的值,只有葉子節點和部分內部節點所對應的鍵才有相關的值。
Trie這個術語來自於retrieval。根據詞源學,trie的發明者Edward Fredkin把它讀作/ˈtriː/ "tree"。但是,其他作者把它讀作/ˈtraɪ/ "try"。
與二叉查找樹不同,Trie樹的鍵不是直接保存在節點中,而是由節點在樹中的位置決定。一個節點的所有子孫都有相同的前綴,也就是這個節點對應的字符串,而根節點對應空字符串。一般情況下,不是所有的節點都有對應的值,只有葉子節點和部分內部節點所對應的鍵才有相關的值。
Trie樹優點是最大限度地減少無謂的字符串比較,查詢效率比較高。核心思想是空間換時間,利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的。
(1) 插入、查找的時間復雜度均為O(N),其中N為字符串長度。
(2) 空間復雜度是26^n級別的,非常龐大(可采用雙數組實現改善)。
它有3個基本性質:
根節點不包含字符,除根節點外每一個節點都只包含一個字符。
從根節點到某一節點,路徑上經過的字符連接起來,為該節點對應的字符串。
每個節點的所有子節點包含的字符都不相同。
【rax與壓縮trie】
Trie樹其實依然比較浪費空間,有人曾經反饋他們在實際的項目發現,隨着key的數量的增加,發現Trie樹會占用大量的內存和空間。
看標准的trie樹存儲的結構如圖:
其實可以將沒有分支的鏈路節點合並,如圖:
Redis中存儲的trie樹就是這種壓縮過的結構,也就是Rax樹。
【結構】
計算機對於Radix樹的處理是以bit(或二進制數字)來讀取的。一次被對比r個bit,2的r次方是radix樹的基數。這也是基數樹的這個名字的由來。
現在把上面的三個單詞變成二進制的樣子,然后一位一位的看:
dog: 01100100 01101111 01100111
doge: 01100100 01101111 01100111 01100101
dogs: 01100100 01101111 01100111 01110011
按照字符串的比對,會發現dog是dogs和doge的子串。但我們現在比對二進制,一位一位的比對,會發現dog和doge是在第二十五位的時候不一樣的。dogs和doge是在第二十八位不一樣的。這就是計算機的方式。
此時來看看Rax中的幾種結構,之前說過Rax是壓縮過的trie,它的結構有三種類型,根節點、葉子結點、中間節點。有些中間節點帶value,有些是結構性需要,沒有value。
struct raxNode { int<1> isKey; // 是否有key,沒key是根節點 int<1> isNull; // 沒有對應的value,中間節點 int<1> isCompressed; // 是否存儲壓縮 int<29> size; // 葉子節點數量或者是壓縮字符串長度 byte[] data; // 用於存儲路由鍵、葉子節點指針、value }
redis中的rax在結構上不是標准的Radix Tree,如果一個中間節點有多個葉子節點,路由鍵就是一個字符;如果只有一個葉子節點,路由鍵就是一個字符串。
只有一個葉子節點的時候,就是壓縮節點(多個字符壓在一起的字符串),下圖中的深藍色節點就是壓縮節點:
isCompressed記錄了這個節點是不是壓縮結構,壓縮或者不壓縮會直接反映在data的結構里。
--對於壓縮的節點:
struct data { optional struct { // 取決於raxNode的size字段是否為0 byte[] childKey; // 路由鍵 raxNode* childNode; // 子節點指針 } child; optional string value; // 取決於raxNode的isNull字段 }
如果壓縮節點后面沒有節點了,childNode就不存在,如果是中間節點,那么value字段也不存在。這里要注意的是,中間節點其實可以是壓縮節點的,但是不能有多個葉子節點,如果是多個葉子節點,那就不能是壓縮節點,而必須用單個字符作為路由鍵。
--對於非壓縮的節點:
葉子節點有多個,就不是壓縮節點,存在多個路由鍵,一個鍵就是一個字符。
struct data { byte[] childKeys; // 路由鍵字符列表 raxNode*[] childNodes; // 多個葉子節點指針 optional string value; // 取決於raxNode的isNull字段 }
結構如圖所示:
【參考】
《Redis深度歷險 核心原理與應用實踐》
https://developer.aliyun.com/article/38231
https://cloud.tencent.com/developer/article/1597128