Redis內部數據結構實現解析

Redis目前在Key-Value存儲以及緩存系統中有着非常廣泛的應用,且以高效快速著稱。不同於其他Key-Value數據庫,Redis提供了豐富的數據結構類型,value可以是字符串、列表、哈希和有序集等,爲用戶操作帶來了極大的便利。本文希望通過分析其內部數據結構及算法的實現機制,來揭示其高性能的背後的原因。

動態字符串

由於所有的Key都是字符串,字符串在Redis中的應用非常廣泛。Redis底層使用sds(Simple Dynamic String)表示簡單字符串,從而替代C語言中的char *類型,以支持高效的字符串擴展操作。

sds的實現方式如下,其中sds是char *的別名。當新增一個value爲字符串類型的鍵值對時,Redis將value存儲在一個adshdr實例中,buf[]存儲value的實際數據,爲buf[]分配的空間長度與value當前所佔空間相同,即len爲value的長度(包括字符串末尾的’\0’),free爲0。在創建adshdr實例完成後,函數將指向buf[]的指針返回給sds,以便於管理adshdr結構體實例的增、刪、改、查等操作。

<p><pre name="code" class="cpp">typedef char *sds;
struct sdshdr {
int len; 		// buf 已使用長度
int free; 		// buf 可用長度
char buf[];	  	// 所保存的字符串數據
};



當對value進行append操作時,顯然,最初創建時分配給sds的空間已無法滿足存儲要求,因而Redis會爲buf[]動態分配存儲空間。當新字符串的總長度小於時sds最大預分配長度時,新的存儲空間所能存放的字符串長度爲(oldStr.length + appendStr.length) * 2 + 1,其中後面的“+1”用來存放‘\0’;否則,就分配appendStr的長度 + 最大預分配長度 的空間。同時,還要同步更新實例中len和free的值,如果buf[]的地址發生了改變,也要更新sds的值。

由於sds中預分配機制的存在,避免了每次value的追加都要進行內存分配步驟,很大地提高了append操作的效率。除此之外,相比於C的char *,sds使用len變量記錄字符已使用的長度,從而使用戶可以快速地獲取字符串的長度信息。

字典

Redis的字典(Dictionary)的底層實現利用的是hash表。字典的結構體如下:

typedef struct dict {
dictType *type;		// hash表的類型,可以是string, list等
void *privdata; 		// 類型處理函數的私有數據
dictht ht[2]; 		// 哈希表(2 個)
int rehashidx; 		// 是否在進行rehash 的標誌位
int iterators; 		// 當前運行的安全迭代器數量
} dict;

哈希表的結構如下:

typedef struct dictht {
dictEntry **table; 		// 哈希表節點指針數組(即bucket)
unsigned long size; 	// 指針數組的大小
// 指針數組的長度掩碼,用於計算索引值
unsigned long sizemask;
unsigned long used; 	// 哈希表現有的節點數量
} dictht;



哈希表中的每一個元素指向一個表結點,即dictEntry結構體實例的指針,每個dictEntry由一個鍵值對和一個指向後繼表節點的指針next構成,從而可以使用鏈地址法處理哈希衝突。


(圖來自《Redis內存存儲結構分析》)

 

一個字典實例中有兩個hash表,當在空字典中添加第一條數據時,Redis會爲ht[0]所指向的table分配一定的存儲空間;隨着數據量的逐漸增多,字典的索引利用率達到一定比例時,需要對字典進行rehash的操作,從而保證字典的查詢和插入效率保持在較高的水準,即字典的expend。這時,會創建一個更大的table(空間至少爲ht[0]的兩倍),其地址被賦給ht[1],字典中的數據將由ht[0]遷移到ht[1]。

在遷移過程開始時,rehashidx值設置爲0,標誌着字典處於遷移狀態;遷移結束後,ht[0]的數據將被清空,並將ht[1]更改爲新的ht[0],ht[1]指向一個新的空hash表,rehashidx的值重新設爲-1,標誌着字典不在數據遷移狀態中。

如果table中的數據量比較大,就會出現遷移時間過長的問題,致使在一個相對較長的時間段內,用戶無法進行其他操作。因此在實際中,數據遷移是分階段完成的,rehashidx會記錄rehash進行到ht[0]的哪個索引位置上。在遷移過程中,若對字典進行添加操作,則新的節點將會添加到ht[1]中;若對字典進行刪除或查找操作,則需要在ht[0]和ht[1]上同時進行。

由於hash表中數據量是動態變化的,當索引的利用率較低時,也可以通過rehash進行字典收縮(shrink)操作,即用一個將數據遷移到一個更小的table中。不同的是,字典的expend操作是自動觸發的,而shrink是通過程序調用執行的。

跳躍表

       跳躍表是一種隨機化數據結構,基於並聯的列表,其插入、查找、刪除的時間複雜度均爲log(n),可與二叉樹相媲美。跳躍表主要由表頭、節點和表尾組成,其節點分佈在多層鏈表中。

       跳躍表由結構體zskiplist定義:

typedef struct zskiplist {
struct zskiplistNode *header, *tail;	// 頭節點,尾節點
unsigned long length; 				// 節點數量
int level; 							// 表中節點的最大層數
} zskiplist;

表的節點由zskiplistNode定義,每個節點中存有一個Score-Member指針對,score爲跳躍表的索引,*obj指向存儲值member的域。

typedef struct zskiplistNode {
robj *obj;		// member 對象
double score;
struct zskiplistNode *backward; 	// 後退指針
// 層
struct zskiplistLevel {
struct zskiplistNode *forward; 	// 前進指針
unsigned int span; 			// 本層兩個節點的間隔
} level[];
} zskiplistNode;

跳躍表在Redis中應用的結構示意圖如下:


(圖來自 《Redis設計與實現》)


跳躍表在Redis中主要用作有序集類型的底層數據結構,通過層層遍歷查找與給定score值相一致的節點,從而獲取member的值。跳躍表編碼的有序集由一個dictionary和skiplist構成。首先通過dictionary查找出score的值,然後再根據score值在skiplist查取想要獲得的value。

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