dict是一個用於維護key和value映射關係的數據結構。redis的一個database中全部的key到value的映射,就是使用一個dict來維護的。不過,他在redis被使用的地方還不少,好比,一個redis hash結構,當它的field較多的時候,便會採用dict來存儲。再好比,redis配合使用dict和skiplist來共同維護一個zset。redis
在redis中,dict也是一個基於哈希表的算法。和傳統的哈希算法相似,它採用某個哈希函數從key計算獲得哈希表中的位置,用拉鍊發解決衝突,並在元素數量超過裝載因子的時候rehash。redis的hash表最顯著的一個特色,就在於它哈希表的重哈希,採用了一種增量式重哈希的方法,能夠避免在須要擴容時一次性對全部hash表中元素重哈希,致使正常iud操做阻塞。算法
dict的數據結構sql
typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry; typedef struct dictType { unsigned int (*hashFunction)(const void *key); void *(*keyDup)(void *privdata, const void *key); void *(*valDup)(void *privdata, const void *obj); int (*keyCompare)(void *privdata, const void *key1, const void *key2); void (*keyDestructor)(void *privdata, void *key); void (*valDestructor)(void *privdata, void *obj); } dictType; /* This is our hash table structure. Every dictionary has two of this as we * implement incremental rehashing, for the old to the new table. */ typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht; typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; /* rehashing not in progress if rehashidx == -1 */ int iterators; /* number of iterators currently running */ } dict;
具體結構圖以下數組
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1);
}數據結構
int dictRehash(dict *d, int n) { int empty_visits = n*10; /* Max number of empty buckets to visit. */ if (!dictIsRehashing(d)) return 0; while(n-- && d->ht[0].used != 0) { dictEntry *de, *nextde; /* Note that rehashidx can't overflow as we are sure there are more * elements because ht[0].used != 0 */ assert(d->ht[0].size > (unsigned long)d->rehashidx); while(d->ht[0].table[d->rehashidx] == NULL) { d->rehashidx++; if (--empty_visits == 0) return 1; } de = d->ht[0].table[d->rehashidx]; /* Move all the keys in this bucket from the old to the new hash HT */ while(de) { unsigned int h; nextde = de->next; /* Get the index in the new hash table */ h = dictHashKey(d, de->key) & d->ht[1].sizemask; de->next = d->ht[1].table[h]; d->ht[1].table[h] = de; d->ht[0].used--; d->ht[1].used++; de = nextde; } d->ht[0].table[d->rehashidx] = NULL; d->rehashidx++; } /* Check if we already rehashed the whole table... */ if (d->ht[0].used == 0) { zfree(d->ht[0].table); d->ht[0] = d->ht[1]; _dictReset(&d->ht[1]); d->rehashidx = -1; return 0; } /* More to rehash... */ return 1; }
上文說道,漸進性rehash不是一次性完成的,是穿插在每次對於hash操做的時候,漸進的完成的。app
1.爲ht[1] 分配空間,讓字典同時持有ht[0] ht[1]兩個hash表ide
2.在dict中把rehashidx設置爲0,表示下一個rehash操做ht[0]->table[0],rehash工做開始。函數
3.在rehash進行期間,每次對hash執行crud操做時,程序除了執行指定的操做前,要先調用_dictRehashStep(dict *d) 執行單次rehash。性能
4.dictRehash傳入的n表示每次rehash一個桶(有元素的桶,當沒有元素時,最多遍歷10*n個),當找到一個非空桶時,開始把桶中的鏈表拆分紅兩半,分別存入ht[1]->table[]的兩個位置(取決於更高一個bit的值),而後把rehashidx的值更新(實際上是在遍歷桶的時候更新的) 返回1 表示還需rehash。ui
5.隨着rehash的不斷操做,最終在某個操做後,rehash完成,rehashidx設置爲-1,返回 0.
在rehash期間 delete find update 會在ht[0]和ht[1]中都進行查找,add的操做只會添加到ht[1]中。
redis裏面是用skiplist是爲了實現zset這種對外的數據結構。zset提供的操做很是豐富,能夠知足許多業務場景,同時也意味着zset相對來講實現比較複雜。
如圖,跳錶的底層是一個順序鏈表,每隔一個節點有一個上層的指針指向下下一個節點,並層層向上遞歸。這樣設計成相似樹形的結構,可使得對於鏈表的查找能夠到達二分查找的時間複雜度。
按照上面的生成跳錶的方式上面每一層節點的個數是下層節點個數的一半,這種方式在插入數據的時候有很大的問題。就是插入一個新節點會打亂上下相鄰兩層鏈表節點個數嚴格的2:1的對應關係。若是要維持這種嚴格對應關係,就必須從新調整整個跳錶,這會讓插入/刪除的時間複雜度從新退化爲O(n)。
爲了解決這一問題,skiplist他不要求上下兩層鏈表之間個數的嚴格對應關係,他爲每一個節點隨機出一個層數。好比,一個節點的隨機出的層數是3,那麼就把它插入到三層的空間上,以下圖。
那麼,這就產生了一個問題,每次插入節點時隨機出一個層數,真的能保證跳錶良好的性能能麼,
首先跳錶隨機數的產生,不是一次執行就產生的,他有本身嚴格的計算過程,
1首先每一個節點都有最下層(第1層)指針
2若是一個節點有第i層指針,那麼他有第i層指針的機率爲p。
3節點的最大層數不超過MaxLevel
咱們注意到,每一個節點的插入過程都是隨機的,他不依賴於其餘節點的狀況,即skiplist造成的結構和節點插入的順序無關。
這樣造成的skiplist查找的時間複雜度約爲O(log n)。
爲了支持排名rank查詢,redis中對skiplist作了擴展,使得根據排名可以快速查到數據,或者根據分數查到數據以後容易得到排名,兩者都是O(log n)。
typedef struct zset{ //跳躍表 zskiplist *zsl; //字典 dict *dice; } zset;
dict的key保存元素的值,字典的value保存元素的score,跳錶節點的robj保存元素的成員,節點的score保存對應score。而且會經過指針來共享元素相同的robj和score。
//server.h
#define ZSKIPLIST_MAXLEVEL 32 #define ZSKIPLIST_P 0.25 typedef struct zskiplistNode { robj *obj; double score; struct zskiplistNode *backward; struct zskiplistLevel { struct zskiplistNode *forward; unsigned int span; } level[]; } zskiplistNode; typedef struct zskiplist { struct zskiplistNode *header, *tail; unsigned long length; int level; } zskiplist;
開頭定義了兩個常量 ZSKIPLIST_MAXLEVEL和ZSKIPLIST_P,即上文所提到的p和maxlevel。
zskiplistNode表示skiplist的節點結構
zskiplist 定義了skiplist的外觀,包含
上圖就是redis中一個skiplist可能的結構,括號中的數字表明 level數組中span的值,即跨越了多少個節點。
假設咱們在這個skiplist中查找score=89的元素,在查找路徑上,咱們只須要吧全部的level指針對應的span值求和,就能夠獲得對應的排名;相反,若是查找排名的時候,只須要不斷累加span保證他不超過指定的值就能夠求得對應的節點元素。
redis中使用intset實現數量較少數字的set。
set-max-intset-entries 512
實際上 intset是一個由整數組成的有序集合,爲了快速查找元素,數組是有序的,用二分查找判斷一個元素是否在這個結合上。在內存分配上與ziplist相似,用一塊連續的內存保存數組元素,而且對於大整數和小證書 採用了不一樣的編碼。
結構以下
//intset.h typedef struct intset { uint32_t encoding; uint32_t length; int8_t contents[]; } intset; #define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t))
encoding 數據編碼 表示intset中的每一個元素用幾個字節存儲。(INTSET_ENC_INT16 用兩個字節存儲,即兩個contents數組位置 INTSET_ENC_INT32表示4個字節 INTSET_ENC_INT64表示8個字節)
length 表示inset中元素的個數
contents 柔性數組,表示存儲的實際數據,數組長度 = encoding * length。
另外,intset可能會隨着數據的添加改編他的編碼,最開始建立的inset使用 INTSET_ENC_INT16編碼。
如上圖 intset採用小端存儲。
關於插入邏輯。
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) { uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 1; /* Upgrade encoding if necessary. If we need to upgrade, we know that * this value should be either appended (if > 0) or prepended (if < 0), * because it lies outside the range of existing values. */ if (valenc > intrev32ifbe(is->encoding)) { /* This always succeeds, so we don't need to curry *success. */ return intsetUpgradeAndAdd(is,value); } else { /* Abort if the value is already present in the set. * This call will populate "pos" with the right position to insert * the value when it cannot be found. */ if (intsetSearch(is,value,&pos)) { if (success) *success = 0; return is; } is = intsetResize(is,intrev32ifbe(is->length)+1); if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1); } _intsetSet(is,pos,value); is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; }
intsetadd在intset中添加新元素value。若是value在添加前已經存在,則不會重複添加,這個時候success設置值爲0
若是要添加的元素編碼比當前intset的編碼大。調用intsetUpgradeAndAdd將intset的編碼進行增加,而後插入。
調用intsetSearch 若是能查找到,不會重複添加。沒查到調用intsetResize對其進行擴容(realloc),同時intsetMoveTail將帶插入位置後面的元素統一貫後移動一個位置。返回值是一個新的intset指針,替換原來的intset指針,總的時間複雜度爲O(n)。