幾個月前的面試問到了redis 的底層,現在做個筆記!
Redis 中所有資料都儲存在 DB 中,一個 Redis 預設最多支援 16 個 DB。Redis 中的每個 DB 都對應一個 redisDb 結構,即每個 Redis 例項,預設有 16 個 redisDb。使用者訪問時,預設使用的是 0 號 DB,可以通過 select $dbID 在不同 DB 之間切換。
redisDb 主要包括 2 個核心 dict 字典、3 個非核心 dict 字典、dbID 和其他輔助屬性。2 個核心 dict 包括一個 dict 主字典和一個 expires 過期字典。主 dict 字典用來儲存當前 DB 中的所有資料,它將 key 和各種資料型別的 value 關聯起來,該 dict 也稱 key space。過期字典用來儲存過期時間 key,存的是 key 與過期時間的對映。日常的資料儲存和訪問基本都會訪問到 redisDb 中的這兩個 dict。
3 個非核心 dict 包括一個欄位名叫 blocking_keys 的阻塞 dict,一個欄位名叫 ready_keys 的解除阻塞 dict,還有一個是欄位名叫 watched_keys 的 watch 監控 dict。
在執行 Redis 中 list 的阻塞命令 blpop、brpop 或者 brpoplpush 時,如果對應的 list 列表為空,Redis 就會將對應的 client 設為阻塞狀態,同時將該 client 新增到 DB 中 blocking_keys 這個阻塞 dict。所以該 dict 儲存的是處於阻塞狀態的 key 及 client 列表。
在執行 Redis 中 list 的阻塞命令 blpop、brpop 或者 brpoplpush 時,如果對應的 list 列表為空,Redis 就會將對應的 client 設為阻塞狀態,同時將該 client 新增到 DB 中 blocking_keys 這個阻塞 dict。所以該 dict 儲存的是處於阻塞狀態的 key 及 client 列表。
當有其他呼叫方在向某個 key 對應的 list 中增加元素時,Redis 會檢測是否有 client 阻塞在這個 key 上,即檢查 blocking_keys 中是否包含這個 key,如果有則會將這個 key 加入 read_keys 這個 dict 中。同時也會將這個 key 儲存到 server 中的一個名叫 read_keys 的列表中。這樣可以高效、不重複的插入及輪詢。
當 client 使用 watch 指令來監控 key 時,這個 key 和 client 就會被儲存到 watched_keys 這個 dict 中。redisDb 中可以儲存所有的資料型別,而 Redis 中所有資料型別都是存放在一個叫 redisObject 的結構中。
redisObject 由 5 個欄位組成。
type:即 Redis 物件的資料型別,目前支援 7 種 type 型別,分別為
OBJ_STRING
OBJ_LIST
OBJ_SET
OBJ_ZSET
OBJ_HASH
OBJ_MODULE
OBJ_STREAM
encoding:Redis 物件的內部編碼方式,即內部資料結構型別,目前支援 10 種編碼方式包括
OBJ_ENCODING_RAW
OBJ_ENCODING_INT
OBJ_ENCODING_HT
OBJ_ENCODING_ZIPLIST 等。
LRU:儲存的是淘汰資料用的 LRU 時間或 LFU 頻率及時間的資料。
refcount:記錄 Redis 物件的引用計數,用來表示物件被共享的次數,共享使用時加 1,不再使用時減 1,當計數為 0 時表明該物件沒有被使用,就會被釋放,回收記憶體。
ptr:它指向物件的內部資料結構。比如一個代表 string 的物件,它的 ptr 可能指向一個 sds 或者一個 long 型整數。
前面講到,Redis 中的資料實際是存在 DB 中的 2 個核心 dict 字典中的。實際上 dict 也是 Redis 的一種使用廣泛的內部資料結構。
Redis 中的 dict,類似於 Memcached 中 hashtable。都可以用於 key 或元素的快速插入、更新和定位。dict 字典中,有一個長度為 2 的雜湊表陣列,日常訪問用 0 號雜湊表,如果 0 號雜湊表元素過多,則分配一個 2 倍 0 號雜湊表大小的空間給 1 號雜湊表,然後進行逐步遷移,rehashidx 這個欄位就是專門用來做標誌遷移位置的。在雜湊表操作中,採用單向連結串列來解決 hash 衝突問題。dict 中還有一個重要欄位是 type,它用於儲存 hash 函式及 key/value 賦值、比較函式。
dictht 中的 table 是一個 hash 表陣列,每個桶指向一個 dictEntry 結構。dictht 採用 dictEntry 的單向連結串列來解決 hash 衝突問題。
dictht 是以 dictEntry 來存 key-value 對映的。其中 key 是 sds 字串,value 為儲存各種資料型別的 redisObject 結構。
dict 可以被 redisDb 用來儲存資料 key-value 及命令操作的輔助資訊。還可以用來作為一些 Redis 資料型別的內部資料結構。dict 可以作為 set 集合的內部資料結構。在雜湊的元素數超過 512 個,或者雜湊中 value 大於 64 位元組,dict 還被用作為雜湊型別的內部資料結構。
字串是 Redis 中最常見的資料型別,其底層實現是簡單動態字串即 sds。簡單動態字串本質是一個 char*,內部通過 sdshdr 進行管理。sdshdr 有 4 個欄位。len 為字串實際長度,alloc 當前位元組陣列總共分配的記憶體大小。flags 記錄當前位元組陣列的屬性;buf 是儲存字串真正的值及末尾一個 \0。
sds 的儲存 buf 可以動態擴充套件或收縮,字串長度不用遍歷,可直接獲得,修改和訪問都很方便。由於 sds 中字串存在 buf 陣列中,長度由 len 定義,而不像傳統字串遇 0 停止,所以 sds 是二進位制安全的,可以存放任何二進位制的資料。
簡單動態字串 sds 的獲取字串長度很方便,通過 len 可以直接得到,而傳統字串需要對字串進行遍歷,時間複雜度為 O(n)。
sds 相比傳統字串多了一個 sdshdr,對於大量很短的字串,這個 sdshdr 還是一個不小的開銷。在 3.2 版本後,sds 會根據字串實際的長度,選擇不同的資料結構,以更好的提升記憶體效率。當前 sdshdr 結構分為 5 種子型別,分別為 sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。其中 sdshdr5 只有 flags 和 buf 欄位,其他幾種型別的 len 和 alloc 採用從 uint8_t 到 uint64_t 的不同型別,以節省記憶體空間。
sds 可以作為字串的內部資料結構,同時 sds 也是 hyperloglog、bitmap 型別的內部資料結構。
為了節約記憶體,並減少記憶體碎片,Redis 設計了 ziplist 壓縮列表內部資料結構。壓縮列表是一塊連續的記憶體空間,可以連續儲存多個元素,沒有冗餘空間,是一種連續記憶體資料塊組成的順序型記憶體結構。
ziplist 的結構如圖所示,主要包括 5 個部分。
zlbytes 是壓縮列表所佔用的總記憶體位元組數。
Zltail 尾節點到起始位置的位元組數。
Zllen 總共包含的節點/記憶體塊數。
Entry 是 ziplist 儲存的各個資料節點,這些資料點長度隨意。
Zlend 是一個魔數 255,用來標記壓縮列表的結束。
如圖所示,一個包含 4 個元素的 ziplist,總佔用位元組是 100bytes,該 ziplist 的起始元素的指標是 p,zltail 是 80,則第 4 個元素的指標是 P+80。
壓縮列表 ziplist 的儲存節點 entry 的結構如圖,主要有 6 個欄位。
prevRawLen 是前置節點的長度;
preRawLenSize 編碼 preRawLen 需要的位元組數;
len 當前節點的長度;
lensize 編碼 len 所需要的位元組數;
encoding 當前節點所用的編碼型別;
entryData 當前節點資料。
由於 ziplist 是連續緊湊儲存,沒有冗餘空間,所以插入新的元素需要 realloc 擴充套件記憶體,所以如果 ziplist 佔用空間太大,realloc 重新分配記憶體和拷貝的開銷就會很大,所以 ziplist 不適合儲存過多元素,也不適合儲存過大的字串。
因此只有在元素數和 value 數都不大的時候,ziplist 才作為 hash 和 zset 的內部資料結構。其中 hash 使用 ziplist 作為內部資料結構的限制時,元素數預設不超過 512 個,value 值預設不超過 64 位元組。可以通過修改配置來調整 hash_max_ziplist_entries 、hash_max_ziplist_value 這兩個閥值的大小。
zset 有序集合,使用 ziplist 作為內部資料結構的限制元素數預設不超過 128 個,value 值預設不超過 64 位元組。可以通過修改配置來調整 zset_max_ziplist_entries 和 zset_max_ziplist_value 這兩個閥值的大小。
Redis 在 3.2 版本之後引入 quicklist,用以替換 linkedlist。因為 linkedlist 每個節點有前後指標,要佔用 16 位元組,而且每個節點獨立分配記憶體,很容易加劇記憶體的碎片化。而 ziplist 由於緊湊型儲存,增加元素需要 realloc,刪除元素需要記憶體拷貝,天然不適合元素太多、value 太大的儲存。
而 quicklist 快速列表應運而生,它是一個基於 ziplist 的雙向連結串列。將資料分段儲存到 ziplist,然後將這些 ziplist 用雙向指標連線。快速列表的結構如圖所示。
head、tail 是兩個指向第一個和最後一個 ziplist 節點的指標。
count 是 quicklist 中所有的元素個數。
len 是 ziplist 節點的個數。
compress 是 LZF 演算法的壓縮深度。
快速列表中,管理 ziplist 的是 quicklistNode 結構。quicklistNode 主要包含一個 prev/next 雙向指標,以及一個 ziplist 節點。單個 ziplist 節點可以存放多個元素。
快速列表從頭尾讀寫資料很快,時間複雜度為 O(1)。也支援從中間任意位置插入或讀寫元素,但速度較慢,時間複雜度為 O(n)。快速列表當前主要作為 list 列表的內部資料結構。
跳躍表 zskiplist 是一種有序資料結構,它通過在每個節點維持多個指向其他節點的指標,從而可以加速訪問。跳躍表支援平均 O(logN) 和最差 O(n) 複雜度的節點查詢。在大部分場景,跳躍表的效率和平衡樹接近,但跳躍表的實現比平衡樹要簡單,所以不少程式都用跳躍表來替換平衡樹。
如果 sorted set 型別的元素數比較多或者元素比較大,Redis 就會選擇跳躍表來作為 sorted set有序集合的內部資料結構。
跳躍表主要由 zskipList 和節點 zskiplistNode 構成。zskiplist 結構如圖,header 指向跳躍表的表頭節點。tail 指向跳躍表的表尾節點。length 表示跳躍表的長度,它是跳躍表中不包含表頭節點的節點數量。level 是目前跳躍表內,除表頭節點外的所有節點中,層數最大的那個節點的層數。
跳躍表的節點 zskiplistNode 的結構如圖所示。ele 是節點對應的 sds 值,在 zset 有序集合中就是集合中的 field 元素。score 是節點的分數,通過 score,跳躍表中的節點自小到大依次排列。backward 是指向當前節點的前一個節點的指標。level 是節點中的層,每個節點一般有多個層。每個 level 層都帶有兩個屬性,一個是 forwad 前進指標,它用於指向表尾方向的節點;另外一個是 span 跨度,它是指 forward 指向的節點到當前節點的距離。
如圖所示是一個跳躍表,它有 3 個節點。對應的元素值分別是 S1、S2 和 S3,分數值依次為 1.0、3.0 和 5.0。其中 S3 節點的 level 最大是 5,跳躍表的 level 是 5。header 指向表頭節點,tail 指向表尾節點。在查到元素時,累加路徑上的跨度即得到元素位置。在跳躍表中,元素必須是唯一的,但 score 可以相同。相同 score 的不同元素,按照字典序進行排序。
在 sorted set 資料型別中,如果元素數較多或元素長度較大,則使用跳躍表作為內部資料結構。預設元素數超過 128 或者最大元素的長度超過 64,此時有序集合就採用 zskiplist 進行儲存。由於 geo 也採用有序集合型別來儲存地理位置名稱和位置 hash 值,所以在超過相同閥值後,也採用跳躍表進行儲存。
陳波 新浪微博平臺架構技術專家 《300 分鐘吃透分散式快取》
本作品採用《CC 協議》,轉載必須註明作者和本文連結