本文來自程序員歷小冰的投稿node
在此感謝歷小冰同窗的分享程序員
Redis是一個開源的 key-value 存儲系統,它使用六種底層數據結構構建了包含字符串對象、列表對象、哈希對象、集合對象和有序集合對象的對象系統。 redis
今天咱們就經過12張圖來全面瞭解一下它的數據結構和對象系統的實現原理。算法
本文的內容以下:數據庫
首先介紹六種基礎數據結構:動態字符串,鏈表,字典,跳躍表,整數集合和壓縮列表。數組
其次介紹 Redis 的對象系統中的字符串對象(String)、列表對象(List)、哈希對象(Hash)、集合對象(Set)和有序集合對象(ZSet)。緩存
最後介紹 Redis 的鍵空間和過時鍵( expire )實現。bash
Redis 使用動態字符串 SDS 來表示字符串值。下圖展現了一個值爲 Redis 的 SDS結構 :服務器
len: 表示字符串的真正長度(不包含NULL結束符在內)。數據結構
alloc: 表示字符串的最大容量(不包含最後多餘的那個字節)。
flags: 老是佔用一個字節。其中的最低3個bit用來表示header的類型。
buf: 字符數組。
SDS 的結構能夠減小修改字符串時帶來的內存重分配的次數,這依賴於內存預分配和惰性空間釋放兩大機制。
當 SDS 須要被修改,而且要對 SDS 進行空間擴展時,Redis 不只會爲 SDS 分配修改所必需要的空間,還會爲 SDS 分配額外的未使用的空間。
若是修改後, SDS 的長度(也就是len屬性的值)將小於 1MB ,那麼 Redis 預分配和 len 屬性相同大小的未使用空間。
若是修改後, SDS 的長度將大於 1MB ,那麼 Redis 會分配 1MB 的未使用空間。
好比說,進行修改後 SDS 的 len 長度爲20字節,小於 1MB,那麼 Redis 會預先再分配 20 字節的空間, SDS 的 buf數組的實際長度(除去最後一字節)變爲 20 + 20 = 40 字節。當 SDS的 len 長度大於 1MB時,則只會再多分配 1MB的空間。
相似的,當 SDS 縮短其保存的字符串長度時,並不會當即釋放多出來的字節,而是等待以後使用。
鏈表在 Redis 中的應用很是普遍,好比列表對象的底層實現之一就是鏈表。除了鏈表對象外,發佈和訂閱、慢查詢、監視器等功能也用到了鏈表。
Redis 的鏈表是雙向鏈表,示意圖如上圖所示。鏈表是最爲常見的數據結構,這裏就不在細說。
Redis 的鏈表結構的dup 、 free 和 match 成員屬性是用於實現多態鏈表所需的類型特定函數:
dup 函數用於複製鏈表節點所保存的值,用於深度拷貝。
free 函數用於釋放鏈表節點所保存的值。
match 函數則用於對比鏈表節點所保存的值和另外一個輸入值是否相等。
字典被普遍用於實現 Redis 的各類功能,包括鍵空間和哈希對象。其示意圖以下所示。
Redis 使用 MurmurHash2 算法來計算鍵的哈希值,而且使用鏈地址法來解決鍵衝突,被分配到同一個索引的多個鍵值對會鏈接成一個單向鏈表。
Redis 使用跳躍表做爲有序集合對象的底層實現之一。它以有序的方式在層次化的鏈表中保存元素, 效率和平衡樹媲美 —— 查找、刪除、添加等操做均可以在對數指望時間下完成, 而且比起平衡樹來講, 跳躍表的實現要簡單直觀得多。
跳錶的示意圖如上圖所示,這裏只簡單說一下它的核心思想,並不進行詳細的解釋。
如示意圖所示,zskiplistNode 是跳躍表的節點,其 ele 是保持的元素值,score 是分值,節點按照其 score 值進行有序排列,而 level 數組就是其所謂的層次化鏈表的體現。
每一個 node 的 level 數組大小都不一樣, level 數組中的值是指向下一個 node 的指針和 跨度值 (span),跨度值是兩個節點的score的差值。越高層的 level 數組值的跨度值就越大,底層的 level 數組值的跨度值越小。
level 數組就像是不一樣刻度的尺子。度量長度時,先用大刻度估計範圍,再不斷地用縮小刻度,進行精確逼近。
當在跳躍表中查詢一個元素值時,都先從第一個節點的最頂層的 level 開始。好比說,在上圖的跳錶中查詢 o2 元素時,先從o1 的節點開始,由於 zskiplist 的 header 指針指向它。
先從其 level[3] 開始查詢,發現其跨度是 2,o1 節點的 score 是1.0,因此加起來爲 3.0,大於 o2 的 score 值2.0。因此,咱們能夠知道 o2 節點在 o1 和 o3 節點之間。這時,就改用小刻度的尺子了。就用level[1]的指針,順利找到 o2 節點。
整數集合 intset 是集合對象的底層實現之一,當一個集合只包含整數值元素,而且這個集合的元素數量很少時, Redis 就會使用整數集合做爲集合對象的底層實現。
如上圖所示,整數集合的 encoding 表示它的類型,有int16t,int32t 或者int64_t。其每一個元素都是 contents 數組的一個數組項,各個項在數組中按值的大小從小到大有序的排列,而且數組中不包含任何重複項。length 屬性就是整數集合包含的元素數量。
壓縮隊列 ziplist 是列表對象和哈希對象的底層實現之一。當知足必定條件時,列表對象和哈希對象都會以壓縮隊列爲底層實現。
壓縮隊列是 Redis 爲了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構。它的屬性值有:
zlbytes : 長度爲 4 字節,記錄整個壓縮數組的內存字節數。
zltail : 長度爲 4 字節,記錄壓縮隊列表尾節點距離壓縮隊列的起始地址有多少字節,經過該屬性能夠直接肯定尾節點的地址。
zllen : 長度爲 2 字節,包含的節點數。當屬性值小於 INT16_MAX時,該值就是節點總數,不然須要遍歷整個隊列才能肯定總數。
zlend : 長度爲 1 字節,特殊值,用於標記壓縮隊列的末端。
中間每一個節點 entry 由三部分組成:
previous_entry_length : 壓縮列表中前一個節點的長度,和當前的地址進行指針運算,計算出前一個節點的起始地址。
encoding: 節點保存數據的類型和長度
content :節點值,能夠爲一個字節數組或者整數。
上面介紹了 6 種底層數據結構,Redis 並無直接使用這些數據結構來實現鍵值數據庫,而是基於這些數據結構建立了一個對象系統.
這個系統包含字符串對象、列表對象、哈希對象、集合對象和有序集合這五種類型的對象,每一個對象都使用到了至少一種前邊講的底層數據結構。
Redis 根據不一樣的使用場景和內容大小來判斷對象使用哪一種數據結構,從而優化對象在不一樣場景下的使用效率和內存佔用。
Redis 的 redisObject 結構的定義以下所示。
typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; int refcount; void *ptr;} robj;複製代碼
其中 type 是對象類型,包括REDISSTRING, REDISLIST, REDISHASH, REDISSET 和 REDIS_ZSET。
encoding是指對象使用的數據結構,全集以下。
咱們首先來看字符串對象的實現,以下圖所示。
若是一個字符串對象保存的是一個字符串值,而且長度大於32字節,那麼該字符串對象將使用 SDS 進行保存,並將對象的編碼設置爲 raw,如圖的上半部分所示。
若是字符串的長度小於32字節,那麼字符串對象將使用embstr 編碼方式來保存。
embstr 編碼是專門用於保存短字符串的一種優化編碼方式,這個編碼的組成和 raw 編碼一致,都使用 redisObject 結構和 sdshdr 結構來保存字符串,如上圖的下半部所示。
可是 raw 編碼會調用兩次內存分配來分別建立上述兩個結構,而 embstr 則經過一次內存分配來分配一塊連續的空間,空間中一次包含兩個結構。
embstr 只需一次內存分配,並且在同一塊連續的內存中,更好的利用緩存帶來的優點,可是 embstr 是隻讀的,不能進行修改,當一個 embstr 編碼的字符串對象進行 append 操做時, redis 會現將其轉變爲 raw 編碼再進行操做。
列表對象的編碼能夠是 ziplist 或 linkedlist。其示意圖以下所示。
當列表對象能夠同時知足如下兩個條件時,列表對象使用 ziplist 編碼:
列表對象保存的全部字符串元素的長度都小於 64 字節。
列表對象保存的元素數量數量小於 512 個。
不能知足這兩個條件的列表對象須要使用 linkedlist 編碼或者轉換爲 linkedlist 編碼。
哈希對象的編碼可使用 ziplist 或 dict。其示意圖以下所示。
當哈希對象使用壓縮隊列做爲底層實現時,程序將鍵值對緊挨着插入到壓縮隊列中,保存鍵的節點在前,保存值的節點在後。以下圖的上半部分所示,該哈希有兩個鍵值對,分別是 name:Tom 和 age:25。
當哈希對象能夠同時知足如下兩個條件時,哈希對象使用 ziplist 編碼:
哈希對象保存的全部鍵值對的鍵和值的字符串長度都小於64字節。
哈希對象保存的鍵值對數量小於512個。
不能知足這兩個條件的哈希對象須要使用 dict 編碼或者轉換爲 dict 編碼。
集合對象的編碼可使用 intset 或者 dict。
intset 編碼的集合對象使用整數集合最爲底層實現,全部元素都被保存在整數集合裏邊。
而使用 dict 進行編碼時,字典的每個鍵都是一個字符串對象,每一個字符串對象就是一個集合元素,而字典的值所有都被設置爲NULL。以下圖所示。
當集合對象能夠同時知足如下兩個條件時,對象使用 intset 編碼:
集合對象保存的全部元素都是整數值。
集合對象保存的元素數量不超過512個。
不然使用 dict 進行編碼。
有序集合的編碼能夠爲 ziplist 或者 skiplist。
有序集合使用 ziplist 編碼時,每一個集合元素使用兩個緊挨在一塊兒的壓縮列表節點表示,前一個節點是元素的值,第二個節點是元素的分值,也就是排序比較的數值。
壓縮列表內的集合元素按照分值從小到大進行排序,以下圖上半部分所示。
有序集合使用 skiplist 編碼時使用 zset 結構做爲底層實現,一個 zet 結構同時包含一個字典和一個跳躍表。
其中,跳躍表按照分值從小到大保存全部元素,每一個跳躍表節點保存一個元素,其score值是元素的分值。而字典則建立一個一個從成員到分值的映射,字典的鍵是集合成員的值,字典的值是集合成員的分值。經過字典能夠在O(1)複雜度查找給定成員的分值。以下圖所示。
跳躍表和字典中的集合元素值對象都是共享的,因此不會額外消耗內存。
當有序集合對象能夠同時知足如下兩個條件時,對象使用 ziplist 編碼:
有序集合保存的元素數量少於128個;
有序集合保存的全部元素的長度都小於64字節。
不然使用 skiplist 編碼。
Redis 服務器都有多個 Redis 數據庫,每一個Redis 數據都有本身獨立的鍵值空間。每一個 Redis 數據庫使用 dict 保存數據庫中全部的鍵值對。
鍵空間的鍵也就是數據庫的鍵,每一個鍵都是一個字符串對象,而值對象可能爲字符串對象、列表對象、哈希表對象、集合對象和有序集合對象中的一種對象。
除了鍵空間,Redis 也使用 dict 結構來保存鍵的過時時間,其鍵是鍵空間中的鍵值,而值是過時時間,如上圖所示。
經過過時字典,Redis 能夠直接判斷一個鍵是否過時,首先查看該鍵是否存在於過時字典,若是存在,則比較該鍵的過時時間和當前服務器時間戳,若是大於,則該鍵過時,不然未過時。
END
我的公衆號:石杉的架構筆記(ID:shishan100)
歡迎長按下圖關注公衆號:石杉的架構筆記!
公衆號後臺回覆資料,獲取做者獨家祕製學習資料
石杉的架構筆記,BAT架構經驗傾囊相授