Redis Hash資料結構的底層實現
0.前言
redis是KV型的記憶體資料庫, 資料庫儲存的核心就是Hash表, 我們執行select命令選擇一個儲存的db之後, 所有的操作都是以hash表為基礎的, 下面會分析下redis的hash資料結構和實現.
1.hash資料結構
/*Hash表一個節點包含Key,Value資料對 */ typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; /* 指向下一個節點, 連結表的方式解決Hash衝突 */ } 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; typedef struct dictht { dictEntry **table; /* dictEntry*陣列,Hash表 */ unsigned long size; /* Hash表總大小 */ unsigned long sizemask; /* 計算在table中索引的掩碼, 值是size-1 */ unsigned long used; /* Hash表已使用的大小 */ } dictht; typedef struct dict { dictType *type; void *privdata; dictht ht[2]; /* 兩個hash表,rehash時使用*/ long rehashidx; /* rehash的索引, -1表示沒有進行rehash */ int iterators; /* */ } dict;
2.hash資料結構圖
3.漸進式hash說明
dict中ht[2]中有兩個hash表, 我們第一次儲存資料的資料時, ht[0]會建立一個最小為4的hash表, 一旦ht[0]中的size和used相等, 則dict中會在ht[1]建立一個size*2大小的hash表, 此時並不會直接將ht[0]中的資料copy進ht[0]中, 執行的是漸進式rehash, 即在以後的操作(find, set, get等)中慢慢的copy進去, 以後新新增的元素會新增進ht[0], 因此在ht[1]被佔滿的時候定能確保ht[0]中所有的資料全部copy到ht[1]中.
4.建立hash表
建立hash表過程非常簡單,直接呼叫dictCreate函式, 分配一塊記憶體,初始化中間變數即可.
dict *dictCreate(dictType *type, void *privDataPtr)
{
/*分配記憶體*/
dict *d = zmalloc(sizeof(*d));
/*初始化操作*/
_dictInit(d,type,privDataPtr);
return d;
}
5.新增元素
hash表中新增元素,首先判斷空間是否足夠, 然後計算key對應的hash值, 然後將需要新增的key和value放入表中.
int dictAdd(dict *d, void *key, void *val) { /*新增入hash表中, 返回新新增元素的實體結構體*/ dictEntry *entry = dictAddRaw(d,key); if (!entry) return DICT_ERR; /*元素val值放入元素實體結構中*/ dictSetVal(d, entry, val); return DICT_OK; } /* *新增元素實體函式 */ dictEntry *dictAddRaw(dict *d, void *key) { int index; dictEntry *entry; dictht *ht; if (dictIsRehashing(d)) _dictRehashStep(d); /*根據key值計算新元素在hash表中的索引, 返回-1則表示元素已存在, 直接返回NULL*/ if ((index = _dictKeyIndex(d, key)) == -1) return NULL; /*如果在進行rehash過程,則新元素新增到ht[1]中, 否則新增到ht[0]中 */ ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; entry = zmalloc(sizeof(*entry)); entry->next = ht->table[index]; ht->table[index] = entry; ht->used++; /*設定元素key*/ dictSetKey(d, entry, key); return entry; } /* *計算索引的函式 */ static int _dictKeyIndex(dict *d, const void *key) { unsigned int h, idx, table; dictEntry *he; /* 判斷hash表是否空間足夠, 不足則需要擴充套件 */ if (_dictExpandIfNeeded(d) == DICT_ERR) return -1; /* 計算key對應的hash值 */ h = dictHashKey(d, key); for (table = 0; table <= 1; table++) { /*計算索引*/ idx = h & d->ht[table].sizemask; /*遍歷衝突列表, 判斷需要查詢的key是否已經在衝突列表中*/ he = d->ht[table].table[idx]; while(he) { if (dictCompareKeys(d, key, he->key)) return -1; he = he->next; } if (!dictIsRehashing(d)) break; } return idx; } /* *判斷hash表是否需要擴充套件空間 */ static int _dictExpandIfNeeded(dict *d) { /*redis的rehash採用的漸進式hash, rehash時分配了原來兩倍的記憶體空間, 在rehash階段空間必定夠用*/ if (dictIsRehashing(d)) return DICT_OK; /* hash表是空的需要初始化空間, 預設是4*/ if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); /* 已使用空間滿足不了設定的條件*/ 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); } return DICT_OK; } /* *擴充套件空間或者初始化hash表空間 */ int dictExpand(dict *d, unsigned long size) { dictht n; /* 對需要分配大小圓整為2的倍數 */ unsigned long realsize = _dictNextPower(size); /* 如果空間足夠則表明呼叫錯誤 */ if (dictIsRehashing(d) || d->ht[0].used > size) return DICT_ERR; n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; /*hash表為空初始化hash表*/ if (d->ht[0].table == NULL) { d->ht[0] = n; return DICT_OK; } /*新分配的空間放入ht[1], 後面一步一步進行rehash*/ d->ht[1] = n; d->rehashidx = 0; return DICT_OK; }
6.查詢元素
查詢元素過程,首先計算hash值, 然後計算在ht[0]和ht[1]中索引位置, 進行查詢.
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
unsigned int h, idx, table;
if (d->ht[0].size == 0) return NULL;
/*如果正在進行rehash, 執行一次rehash*/
if (dictIsRehashing(d)) _dictRehashStep(d);
h = dictHashKey(d, key);
/*由於可能正在rehash, 因此要從ht[0]和ht[1]中分別進行查詢, 找不到返回NULL*/
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
/*遍歷衝突列表查詢元素*/
while(he) {
if (dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
if (!dictIsRehashing(d)) return NULL;
}
return NULL;
}
7.刪除元素
刪除元素首先查詢元素, 然後將元素從hash表中移除即可, 呼叫dictDelete刪除元素, 會同時刪除元素所佔空間
int dictDelete(dict *ht, const void *key) {
return dictGenericDelete(ht,key,0);
}
static int dictGenericDelete(dict *d, const void *key, int nofree)
{
unsigned int h, idx;
dictEntry *he, *prevHe;
int table;
if (d->ht[0].size == 0) return DICT_ERR;
if (dictIsRehashing(d)) _dictRehashStep(d);
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
prevHe = NULL;
/*查詢元素到元素,進行刪除操作, 並釋放佔用的記憶體*/
while(he) {
if (dictCompareKeys(d, key, he->key)) {
/* Unlink the element from the list */
if (prevHe)
prevHe->next = he->next;
else
d->ht[table].table[idx] = he->next;
if (!nofree) {
dictFreeKey(d, he);
dictFreeVal(d, he);
}
zfree(he);
d->ht[table].used--;
return DICT_OK;
}
prevHe = he;
he = he->next;
}
if (!dictIsRehashing(d)) break;
}
return DICT_ERR; /* not found */
}
hash命令
hash命令操作都比較簡單,需要注意的是當我們建立hash表示預設儲存結構,並不是dict,而是ziplist結構,可以參考redis之Ziplist資料結構,hash_max_ziplist_entries和hash_max_ziplist_value值作為閥值,hash_max_ziplist_entries表示一旦ziplist中元素數量超過該值,則需要轉換為dict結構;hash_max_ziplist_value表示一旦ziplist中資料長度大於該值,則需要轉換為dict結構。