redis設計與實現讀書筆記-數據結構

簡單動態字符串

數據結構:

SDS與C字符串的區別:

C語言使用長度爲N+1的字符數組來表示長度爲N的字符串,並且字符數組的最後一個元素總是空字符'\0'。

C字符串並不記錄自身的長度信息,所以爲了獲取一個C字符串的長度,程序必須遍歷整個字符串,和C字符串不同,因爲SDS在len屬性中記錄了SDS本身的長度,所以獲取一個SDS長度的複雜度僅爲O(1)。與C字符串不同,SDS的空間分配策略完全杜絕了發生緩衝區溢出的可能性:當SDS API需要對SDS進行修改時,API會先檢查SDS的空間是否滿足修改所需的要求,如果不滿足的話,API會自動將SDS的空間擴展至執行修改所需的大小,然後才執行實際的修改操作,所以使用SDS既不需要手動修改SDS的空間大小,也不會出現前面所說的緩衝區溢出問題。

減少修改字符串時帶來的內存重分配次數:

因爲C字符串的長度和底層數組的長度之間存在着這種關聯性,所以每次增長或者縮短一個C字符串,程序都總要對保存這個C字符串的數組進行一次內存重分配操作。爲了避免C字符串的這種缺陷,SDS通過未使用空間解除了字符串長度和底層數組長度之間的關聯:在SDS中,buf數組的長度不一定就是字符數量加一,數組裏面可以包含未使用的字節,而這些字節的數量就由SDS的free屬性記錄。

通過未使用空間,SDS實現了空間預分配和惰性空間釋放兩種優化策略。

空間預分配:

❑如果對SDS進行修改之後,SDS的長度(也即是len屬性的值)將小於1MB,那麼程序分配和len屬性同樣大小的未使用空間,這時SDS len屬性的值將和free屬性的值相同。舉個例子,如果進行修改之後,SDS的len將變成13字節,那麼程序也會分配13字節的未使用空間,SDS的buf數組的實際長度將變成13+13+1=27字節(額外的一字節用於保存空字符)。

❑如果對SDS進行修改之後,SDS的長度將大於等於1MB,那麼程序會分配1MB的未使用空間。舉個例子,如果進行修改之後,SDS的len將變成30MB,那麼程序會分配1MB的未使用空間,SDS的buf數組的實際長度將爲30MB+1MB+1byte。

惰性空間釋放:

惰性空間釋放用於優化SDS的字符串縮短操作:當SDS的API需要縮短SDS保存的字符串時,程序並不立即使用內存重分配來回收縮短後多出來的字節,而是使用free屬性將這些字節的數量記錄起來,並等待將來使用。通過惰性空間釋放策略,SDS避免了縮短字符串時所需的內存重分配操作,併爲將來可能有的增長操作提供了優化。

二進制安全

C字符串中的字符必須符合某種編碼(比如ASCII),並且除了字符串的末尾之外,字符串裏面不能包含空字符,否則最先被程序讀入的空字符將被誤認爲是字符串結尾,這些限制使得C字符串只能保存文本數據,而不能保存像圖片、音頻、視頻、壓縮文件這樣的二進制數據。

爲了確保Redis可以適用於各種不同的使用場景,SDS的API都是二進制安全的(binary-safe),所有SDS API都會以處理二進制的方式來處理SDS存放在buf數組裏的數據,程序不會對其中的數據做任何限制、過濾、或者假設,數據在寫入時是什麼樣的,它被讀取時就是什麼樣。

鏈表

鏈表

提供了高效的節點重排能力,以及順序性的節點訪問方式,並且可以通過增刪節點來靈活地調整鏈表的長度。鏈表在Redis中的應用非常廣泛,比如列表鍵的底層實現之一就是鏈表。

除了鏈表鍵之外,發佈與訂閱、慢查詢、監視器等功能也用到了鏈表,Redis服務器本身還使用鏈表來保存多個客戶端的狀態信息,以及使用鏈表來構建客戶端輸出緩衝區(output buffer)

鏈表和鏈表節點的實現

一個節點:

節點列表,雙端、無環、具有頭爲指針的鏈表:

 

字典

Redis的數據庫就是使用字典來作爲底層實現的,對數據庫的增、刪、查、改操作也是構建在對字典的操作之上的。

除了用來表示數據庫之外,字典還是哈希鍵的底層實現之一,當一個哈希鍵包含的鍵值對比較多,又或者鍵值對中的元素都是比較長的字符串時,Redis就會使用字典作爲哈希鍵的底層實現。

字典的實現

特別像hashmap的數據結構,但是也有不同,數據結構裏邊有個ht屬性,是一個包含兩個項的數組,數組中的每個項都是一個dictht哈希表,一般情況下,字典只使用ht[0]哈希表,ht[1]哈希表只會在對ht[0]哈希表進行rehash時使用,使用過程就是ht[0]進行hash計算到ht[1]中,清空ht[0],把ht[0]編程ht[1],ht[1]編程ht[0]。

解決鍵衝突

Redis的哈希表使用鏈地址法(separate chaining)來解決鍵衝突,每個哈希表節點都有一個next指針,多個哈希表節點可以用next指針構成一個單向鏈表,被分配到同一個索引上的多個節點可以用這個單向鏈表連接起來,這就解決了鍵衝突的問題。

因爲dictEntry節點組成的鏈表沒有指向鏈表表尾的指針,所以爲了速度考慮,程序總是將新節點添加到鏈表的表頭位置(複雜度爲O(1)),排在其他已有節點的前面。

 

跳躍表

跳躍表(skiplist)是一種有序數據結構,它通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。跳躍表支持平均O(logN)、最壞O(N)複雜度的節點查找

和鏈表、字典等數據結構被廣泛地應用在Redis內部不同,Redis只在兩個地方用到了跳躍表,一個是實現有序集合鍵,另一個是在集羣節點中用作內部數據結構,除此之外,跳躍表在Redis裏面沒有其他用途。

跳躍表

 

整數集合

整數集合(intset)是集合鍵的底層實現之一,當一個集合只包含整數值元素,並且這個集合的元素數量不多時,Redis就會使用整數集合作爲集合鍵的底層實現。在數組中按值的大小從小到大有序地排列,並且數組中不包含任何重複項。

升級

每當我們要將一個新元素添加到整數集合裏面,並且新元素的類型比整數集合現有所有元素的類型都要長時,整數集合需要先進行升級(upgrade),然後才能將新元素添加到整數集合裏面。

因爲每次向整數集合添加新元素都可能會引起升級,而每次升級都需要對底層數組中已有的所有元素進行類型轉換,所以向整數集合添加新元素的時間複雜度爲O(N)。

降級

整數集合不支持降級操作,一旦對數組進行了升級,編碼就會一直保持升級後的狀態。

整數集合是集合鍵的底層實現之一。

❑整數集合的底層實現爲數組,這個數組以有序、無重複的方式保存集合元素,在有需要時,程序會根據新添加元素的類型,改變這個數組的類型。

❑升級操作爲整數集合帶來了操作上的靈活性,並且儘可能地節約了內存。

❑整數集合只支持升級操作,不支持降級操作。

壓縮列表

壓縮列表(ziplist)是列表鍵和哈希鍵的底層實現之一。當一個列表鍵只包含少量列表項,並且每個列表項要麼就是小整數值,要麼就是長度比較短的字符串,那麼Redis就會使用壓縮列表來做列表鍵的底層實現。

當一個哈希鍵只包含少量鍵值對,比且每個鍵值對的鍵和值要麼就是小整數值,要麼就是長度比較短的字符串,那麼Redis就會使用壓縮列表來做哈希鍵的底層實現。

壓縮列表是Redis爲了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型(sequential)數據結構。一個壓縮列表可以包含任意多個節點(entry),每個節點可以保存一個字節數組或者一個整數值。

壓縮列表是一種爲節約內存而開發的順序型數據結構。

❑壓縮列表被用作列表鍵和哈希鍵的底層實現之一。

❑壓縮列表可以包含多個節點,每個節點可以保存一個字節數組或者整數值。

❑添加新節點到壓縮列表,或者從壓縮列表中刪除節點,可能會引發連鎖更新操作,但這種操作出現的機率並不高。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章