Redis 字典結構
1. 介紹
字典又稱爲符號表(symbol table)、關聯數組(associative array)或映射(map),是一種用於保存鍵值對(key-value pair)的抽象數據結構。例如:redis中的所有key到value的映射,就是通過字典結構維護,還有hash類型的鍵值。
通過redis中的命令感受一下哈希鍵。
127.0.0.1:6379> HSET user name Mike
(integer) 1
127.0.0.1:6379> HSET user passwd 123456
(integer) 1
127.0.0.1:6379> HSET user sex male
(integer) 1
127.0.0.1:6379> HLEN user //user就是一個包含3個鍵值對的哈希鍵
(integer) 3
127.0.0.1:6379> HGETALL user
1) "name"
2) "Mike"
3) "passwd"
4) "123456"
5) "sex"
6) "male"
user鍵在底層實現就是一個字典,字典包含3個鍵值對。
2. 字典的實現
redis的字典是由哈希表實現的,一個哈希表有多個節點,每個節點保存一個鍵值對。
2.1 哈希表
redis中哈希表定義在dict.h/dictht中。
typedef struct dictht { //哈希表
dictEntry **table; //存放一個數組的地址,數組存放着哈希表節點dictEntry的地址。
unsigned long size; //哈希表table的大小,初始化大小爲4
unsigned long sizemask; //用於將哈希值映射到table的位置索引。它的值總是等於(size-1)。
unsigned long used; //記錄哈希表已有的節點(鍵值對)數量。
} dictht;
2.2 哈希表節點
哈希表的table指向的數組存放這dictEntry類型的地址。也定義在dict.h/dictEntryt中。
ypedef struct dictEntry {
void *key; //key
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; //value
struct dictEntry *next; //指向下一個hash節點,用來解決hash鍵衝突(collision)
} dictEntry;
下圖展現的就是通過鏈接法(chaining)來解決衝突的方法。
2.3 字典
字典結構定義在dict.h/dict中。
typedef struct dict {
dictType *type; //指向dictType結構,dictType結構中包含自定義的函數,這些函數使得key和value能夠存儲任何類型的數據。
void *privdata; //私有數據,保存着dictType結構中函數的參數。
dictht ht[2]; //兩張哈希表。
long rehashidx; //rehash的標記,rehashidx==-1,表示沒在進行rehash
int iterators; //正在迭代的迭代器數量
} dict;
dictType類型保存着 操作字典不同類型key和value的方法 的指針。
typedef struct dictType {
unsigned int (*hashFunction)(const void *key); //計算hash值的函數
void *(*keyDup)(void *privdata, const void *key); //複製key的函數
void *(*valDup)(void *privdata, const void *obj); //複製value的函數
int (*keyCompare)(void *privdata, const void *key1, const void *key2); //比較key的函數
void (*keyDestructor)(void *privdata, void *key); //銷燬key的析構函數
void (*valDestructor)(void *privdata, void *obj); //銷燬val的析構函數
} dictType;
下圖展現的就是剛纔命令user哈希鍵所展現的內部結構:
3. 哈希算法
Thomas Wang認爲好的hash函數具有兩個好的特點:
- hash函數是可逆的。
- 具有雪崩效應,意思是,輸入值1bit位的變化會造成輸出值1/2的bit位發生變化
3.1 計算int整型哈希值的哈希函數
unsigned int dictIntHashFunction(unsigned int key) //用於計算int整型哈希值的哈希函數
{
key += ~(key << 15);
key ^= (key >> 10);
key += (key << 3);
key ^= (key >> 6);
key += ~(key << 11);
key ^= (key >> 16);
return key;
}
3.2 MurmurHash2哈希算法
當字典被用作數據庫的底層實現,或者哈希鍵的底層實現時,redis用MurmurHash2算法來計算哈希值,能產生32-bit或64-bit哈希值。
unsigned int dictGenHashFunction(const void *key, int len) { //用於計算字符串的哈希值的哈希函數
/* 'm' and 'r' are mixing constants generated offline.
They're not really 'magic', they just happen to work well. */
//m和r這兩個值用於計算哈希值,只是因爲效果好。
uint32_t seed = dict_hash_function_seed;
const uint32_t m = 0x5bd1e995;
const int r = 24;
/* Initialize the hash to a 'random' value */
uint32_t h = seed ^ len; //初始化
/* Mix 4 bytes at a time into the hash */
const unsigned char *data = (const unsigned char *)key;
//將字符串key每四個一組看成uint32_t類型,進行運算的到h
while(len >= 4) {
uint32_t k = *(uint32_t*)data;
k *= m;
k ^= k >> r;
k *= m;
h *= m;
h ^= k;
data += 4;
len -= 4;
}
/* Handle the last few bytes of the input array */
switch(len) {
case 3: h ^= data[2] << 16;
case 2: h ^= data[1] << 8;
case 1: h ^= data[0]; h *= m;
};
/* Do a few final mixes of the hash to ensure the last few
* bytes are well-incorporated. */
h ^= h >> 13;
h *= m;
h ^= h >> 15;
return (unsigned int)h;
}
3.3 djb哈希算法
djb哈希算法,算法的思想是利用字符串中的ascii碼值與一個隨機seed,通過len次變換,得到最後的hash值。
unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len) { //用於計算字符串的哈希值的哈希函數
unsigned int hash = (unsigned int)dict_hash_function_seed;
while (len--)
hash = ((hash << 5) + hash) + (tolower(*buf++)); /* hash * 33 + c */
return hash;
}
4. rehash
當哈希表的大小不能滿足需求,就可能會有兩個或者以上數量的鍵被分配到了哈希表數組上的同一個索引上,於是就發生衝突(collision),在Redis中解決衝突的辦法是鏈接法(separate chaining)。但是需要儘可能避免衝突,希望哈希表的負載因子(load factor),維持在一個合理的範圍之內,就需要對哈希表進行擴展或收縮。
Redis對哈希表的rehash操作步驟如下:
- 擴展或收縮
- 擴展:ht[1]的大小爲第一個大於等於ht[0].used * 2的
2n 。 - 收縮:ht[1]的大小爲第一個大於等於ht[0].used的
2n 。
- 擴展:ht[1]的大小爲第一個大於等於ht[0].used * 2的
- 將所有的ht[0]上的節點rehash到ht[1]上。
- 釋放ht[0],將ht[1]設置爲第0號表,並創建新的ht[1]。
源碼再此:
- 擴展操作
static int _dictExpandIfNeeded(dict *d) //擴展d字典,並初始化
{
/* Incremental rehashing already in progress. Return. */
if (dictIsRehashing(d)) return DICT_OK; //正在進行rehash,直接返回
/* If the hash table is empty expand it to the initial size. */
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); //如果字典(的 0 號哈希表)爲空,那麼創建並返回初始化大小的 0 號哈希表
/* If we reached the 1:1 ratio, and we are allowed to resize the hash
* table (global setting) or we should avoid it but the ratio between
* elements/buckets is over the "safe" threshold, we resize doubling
* the number of buckets. */
//1. 字典已使用節點數和字典大小之間的比率接近 1:1
//2. 能夠擴展的標誌爲真
//3. 已使用節點數和字典大小之間的比率超過 dict_force_resize_ratio
if (d->ht[0].used >= d->ht[0].size && (dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
return dictExpand(d, d->ht[0].used*2); //擴展爲節點個數的2倍
}
return DICT_OK;
}
- 收縮操作:
int dictResize(dict *d) //縮小字典d
{
int minimal;
//如果dict_can_resize被設置成0,表示不能進行rehash,或正在進行rehash,返回出錯標誌DICT_ERR
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used; //獲得已經有的節點數量作爲最小限度minimal
if (minimal < DICT_HT_INITIAL_SIZE)//但是minimal不能小於最低值DICT_HT_INITIAL_SIZE(4)
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal); //用minimal調整字典d的大小
}
擴展和收縮操作都調用了dictExpand()函數,該函數通過計算傳入的第二個大小參數進行計算,算出一個最接近
int dictExpand(dict *d, unsigned long size) //根據size調整或創建字典d的哈希表
{
dictht n; /* the new hash table */
unsigned long realsize = _dictNextPower(size); //獲得一個最接近2^n的realsize
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
if (dictIsRehashing(d) || d->ht[0].used > size) //正在rehash或size不夠大返回出錯標誌
return DICT_ERR;
/* Rehashing to the same table size is not useful. */
if (realsize == d->ht[0].size) return DICT_ERR; //如果新的realsize和原本的size一樣則返回出錯標誌
/* Allocate the new hash table and initialize all pointers to NULL */
//初始化新的哈希表的成員
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = 0;
/* Is this the first initialization? If so it's not really a rehashing
* we just set the first hash table so that it can accept keys. */
if (d->ht[0].table == NULL) { //如果ht[0]哈希表爲空,則將新的哈希表n設置爲ht[0]
d->ht[0] = n;
return DICT_OK;
}
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n; //如果ht[0]非空,則需要rehash
d->rehashidx = 0; //設置rehash標誌位爲0,開始漸進式rehash(incremental rehashing)
return DICT_OK;
}
收縮或者擴展哈希表需要將ht[0]表中的所有鍵全部rehash到ht[1]中,但是rehash操作不是一次性、集中式完成的,而是分多次,漸進式,斷續進行的,這樣纔不會對服務器性能造成影響。因此下面介紹漸進式rehash。
5. 漸進式rehash(incremental rehashing)
漸進式rehash的關鍵:
- 字典結構dict中的一個成員rehashidx,當rehashidx爲-1時表示不進行rehash,當rehashidx值爲0時,表示開始進行rehash。
- 在rehash期間,每次對字典的添加、刪除、查找、或更新操作時,都會判斷是否正在進行rehash操作,如果是,則順帶進行單步rehash,並將rehashidx+1。
- 當rehash時進行完成時,將rehashidx置爲-1,表示完成rehash。
源碼在此:
static void _dictRehashStep(dict *d) { //單步rehash
if (d->iterators == 0) dictRehash(d,1); //當迭代器數量不爲0,才能進行1步rehash
}
int dictRehash(dict *d, int n) { //n步進行rehash
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0; //只有rehashidx不等於-1時,才表示正在進行rehash,否則返回0
while(n-- && d->ht[0].used != 0) { //分n步,而且ht[0]上還有沒有移動的節點
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
//確保rehashidx沒有越界,因爲rehashidx是從-1開始,0表示已經移動1個節點,它總是小於hash表的size的
assert(d->ht[0].size > (unsigned long)d->rehashidx);
//第一個循環用來更新 rehashidx 的值,因爲有些桶爲空,所以 rehashidx並非每次都比原來前進一個位置,而是有可能前進幾個位置,但最多不超過 10。
//將rehashidx移動到ht[0]有節點的下標,也就是table[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]; //ht[0]下標爲rehashidx有節點,得到該節點的地址
/* Move all the keys in this bucket from the old to the new hash HT */
//第二個循環用來將ht[0]表中每次找到的非空桶中的鏈表(或者就是單個節點)拷貝到ht[1]中
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; //獲得計算哈希值並得到哈希表中的下標h
//將該節點插入到下標爲h的位置
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
//更新兩個表節點數目計數器
d->ht[0].used--;
d->ht[1].used++;
//將de指向以一個處理的節點
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL; //遷移過後將該下標的指針置爲空
d->rehashidx++; //更新rehashidx
}
/* Check if we already rehashed the whole table... */
if (d->ht[0].used == 0) { //ht[0]上已經沒有節點了,說明已經遷移完成
zfree(d->ht[0].table); //釋放hash表內存
d->ht[0] = d->ht[1]; //將遷移過的1號哈希表設置爲0號哈希表
_dictReset(&d->ht[1]); //重置ht[1]哈希表
d->rehashidx = -1; //rehash標誌關閉
return 0; //表示前已完成
}
/* More to rehash... */
return 1; //表示還有節點等待遷移
}
6. 迭代器
redis在字典結構也定義了迭代器
typedef struct dictIterator {
dict *d; //被迭代的字典
long index; //迭代器當前所指向的哈希表索引位置
int table, safe; //table表示正迭代的哈希表號碼,ht[0]或ht[1]。safe表示這個迭代器是否安全。
dictEntry *entry, *nextEntry; //entry指向當前迭代的哈希表節點,nextEntry則指向當前節點的下一個節點。
/* unsafe iterator fingerprint for misuse detection. */
long long fingerprint; //避免不安全迭代器的指紋標記
} dictIterator;
迭代器分爲安全迭代器和不安全迭代器:
- 非安全迭代器只能進行Get等讀的操作, 而安全迭代器則可以進行iterator支持的任何操作。
- 由於dict結構中保存了safe iterators的數量,如果數量不爲0, 是不能進行下一步的rehash的; 因此安全迭代器的存在保證了遍歷數據的準確性。
- 在非安全迭代器的迭代過程中, 會通過fingerprint方法來校驗iterator在初始化與釋放時字典的hash值是否一致; 如果不一致說明迭代過程中發生了非法操作.
關於dictScan()反向二進制迭代器的原理介紹:Scan迭代器遍歷操作原理
7. 字典結構源碼註釋
由於文件過大,上傳至github上,歡迎瀏覽:字典結構源碼註釋