標籤:
在Redis的內部,資料結構類型值由高效的資料結構和演算法進行支援,並且在Redis自身的構建當中,也大量用到了這些資料結構。
這一部分將對Redis記憶體所使用的資料結構和演算法進行介紹。
動態字串
Sds(Simple Dynamic String,簡單動態字串)
Sds在Redis中的主要作用有以下兩個:
1. 實現字串對象(StringObject);
2. 在Redis程式內部用作char* 類型的替代品;
對比C 字串,sds有以下特性:
–可以高效地執行長度計算(strlen);
–可以高效地執行追加操作(append);
–二進位安全;
•sds會為追加操作進行最佳化:加快追加操作的速度,並降低記憶體配置的次數,代價是多佔用了一些記憶體,而且這些記憶體不會被主動釋放。
typedefchar *sds;
structsdshdr {
// buf已佔用長度
intlen;
// buf剩餘可用長度
intfree;
// 實際儲存字串資料的地方
charbuf[];
};
# 如果新字串的總長度小於SDS_MAX_PREALLOC
# 那麼為字串分配2 倍於所需長度的空間
# 否則就分配所需長度加上SDS_MAX_PREALLOC 數量的空間
雙端鏈表
大部分C 程式都會自己實現一種鏈表類型,Redis也不例外。雙端鏈表還是Redis清單類型的底層實現之一
Note: Redis列表使用兩種資料結構作為底層實現:
1. 雙端鏈表
2. 壓縮列表
因為雙端鏈表佔用的記憶體比壓縮列表要多,所以當建立新的列表鍵時,列表會優先考慮
使用壓縮列表作為底層實現,並且在有需要的時候,才從壓縮列表實現轉換到雙端鏈表實現。
除了實現清單類型以外,雙端鏈表還被很多Redis內部模組所應用:
•事務模組使用雙端鏈表來按順序儲存輸入的命令;
•伺服器模組使用雙端鏈表來儲存多個用戶端;
•訂閱/發送模組使用雙端鏈表來儲存訂閱模式的多個用戶端;
•事件模組使用雙端鏈表來儲存時間事件(time event);
typedefstructlist {
// 表頭指標
listNode*head;
// 表尾指標
listNode*tail;
// 節點數量
unsigned long len;
// 複製函數
void*(*dup)(void *ptr);
// 釋放函數
void(*free)(void *ptr);
// 比對函數
int(*match)(void *ptr, void *key);
} list;
Redis為雙端鏈表實現了一個迭代器,這個迭代器可以從兩個方向對雙端鏈表進行迭代:
雙端鏈表及其節點的效能特性如下:
–節點帶有前驅和後繼指標,訪問前驅節點和後繼節點的複雜度為O(1) ,並且對鏈表
的迭代可以在從表頭到表尾和從表尾到表頭兩個方向進行;
–鏈錶帶有指向表頭和表尾的指標,因此對錶頭和表尾進行處理的複雜度為O(1) ;
–鏈錶帶有記錄節點數量的屬性,所以可以在O(1) 複雜度內返回鏈表的節點數量(長
度);
字典
字典(dictionary),又名映射(map)或關聯陣列(associative array),在Redis中的應用廣泛,使用頻率可以說和SDS 以及雙端鏈表不相上下,基本上各個功能模組都有用到字典的地方。
其中,字典的主要用途有以下兩個:
1. 實現資料庫鍵空間(key space);
2. 用作Hash 類型鍵的其中一種底層實現;
以下兩個小節分別介紹這兩種用途。
Redis的Hash 類型鍵使用以下兩種資料結構作為底層實現:
1. 字典;
2. 壓縮列表;
因為壓縮列表比字典更節省記憶體,所以程式在建立新Hash 鍵時,預設使用壓縮列表作為底層實現,當有需要時,程式才會將底層實現從壓縮列錶轉換到字典。
Redis選擇了高效且實現簡單的雜湊表作為字典的底層實現。
/*
* 字典
**每個字典使用兩個雜湊表,用於實現漸進式rehash
*/
typedefstructdict {
// 特定於類型的處理函數
dictType*type;
// 類型處理函數的私人資料
void*privdata;
// 雜湊表(2 個)
dicththt[2];
// 記錄rehash 進度的標誌,值為-1 表示rehash 未進行
intrehashidx;
// 當前正在運作的安全迭代器數量
intiterators;
} dict;
雜湊表實現
字典所使用的雜湊表實現由dict.h/dictht類型定義:
/*
* 雜湊表
*/
typedefstructdictht {
// 雜湊表節點指標數組(俗稱桶,bucket)
dictEntry**table;
// 指標數組的大小
unsigned long size;
// 指標數組的長度掩碼,用於計算索引值
unsigned long sizemask;
// 雜湊表現有的節點數量
unsigned long used;
} dictht;
每個dictEntry都儲存著一個索引值對,以及一個指向另一個dictEntry結構的指標:
/*
* 雜湊表節點
*/
typedefstructdictEntry {
// 鍵
void*key;
// 值
union{
void*val;
uint64_t u64;
int64_t s64;
} v;
// 鏈往後繼節點
structdictEntry*next;
} dictEntry;
Redis目前使用兩種不同的雜湊演算法:
1. MurmurHash2 32 bit 演算法:這種演算法的分布率和速度都非常好,具體資訊請參考MurmurHash的首頁:http://code.google.com/p/smhasher/ 。
2. 基於djb演算法實現的一個大小寫無關散列演算法:具體資訊請參考
http://www.cse.yorku.ca/~oz/hash.html 。
字典雜湊表所使用的碰撞解決方案被稱之為鏈地址法:
字典收縮和字典擴充的一個區別是:
•字典的擴充操作是自動觸發的(不管是自動擴充還是強制擴充);
•而字典的收縮操作則是由程式手動執行。
字典由索引值對構成的抽象資料結構。
•Redis中的資料庫和雜湊鍵都基於字典來實現。
•Redis字典的底層實現為雜湊表,每個字典使用兩個雜湊表,一般情況下只使用0 號雜湊表,只有在rehash 進行時,才會同時使用0 號和1 號雜湊表。
•雜湊表使用鏈地址法來解決鍵衝突的問題。
• Rehash 可以用於擴充或收縮雜湊表。
•對雜湊表的rehash 是分多次、漸進式地進行的。
跳躍表
它的效率可以和平衡樹媲美——尋找、刪除、添加等操作都可以在對數期望時間下完成,
並且比起平衡樹來說,跳躍表的實現要簡單直觀得多。
•表頭(head):負責維護跳躍表的節點指標。
•跳躍表節點:儲存著元素值,以及多個層。
•層:儲存著指向其他元素的指標。高層的指標越過的元素數量大於等於低層的指標,為了提高尋找的效率,程式總是從高層先開始訪問,然後隨著元素值範圍的縮小,慢慢降低層次。
•表尾:全部由NULL 組成,表示跳躍表的末尾。
看圖想象:
1) 尋找簡單:比如要尋找5,第一層沒找到,第二層定位4—6之間,再降一層則找到5。
2) 插入演算法呢?還是不太確定怎麼實現
跳躍表在Redis的唯一作用,就是實現有序集資料類型。
跳躍表將指向有序集的score 值和member 域的指標作為元素,並以score 值為索引,對有序集元素進行排序。
為了適應自身的需求,Redis基於William Pugh 論文中描述的跳躍表進行了修改,包括:1. score 值可重複。
2. 對比一個元素需要同時檢查它的score 和memeber。
3. 每個節點帶有高度為1 層的後退指標,用於從表尾方向向表頭方向迭代。
redisbook筆記——redis內部資料結構