哈希函數簡介
哈希函數(hash function),又叫散列函數,哈希算法。散列函數把數據“壓縮”成摘要,有的也叫”指紋“,它使數據量變小且數據格式大小也固定。
哈希函數將數據打亂混合,重新創建一個散列值。
我們經常用到的對用戶登錄密碼加密,比如 md5 算法,其實就是一個散列函數。
value = hash_function(input_data),value 這個計算出來的值是大小固定的。
md5("hashmd5") = 46BD4AA9F79D359530D3D873BAC6F3DC,32 位的 md5 值。
當然也有 16 位的 md5 值。
經過哈希函數計算的散列值,會不會出現散列值相同情況?
當然會,這個就是散列值沖突。
所以一個好的哈希函數就很重要,要盡量避免出現散列值沖突。
常用的哈希算法:md5,sha-1,sha-256,sha-512 等等。
哈希表簡介
哈希表可以有很多英文名稱,比如 hashtable,hashmap,symbol table,map 等等,英文名稱雖然不同,但是數據結構基本差不多。
在 map 中,就是一種映射關系。一般保存 key:value 的鍵值對映射關系。
在哈希表中,key 經過哈希函數計算后存儲到哈希表中,然后與 value 值關聯對應。
哈希表的結構組成:數組array + 鏈表list。是一個組合結構。
比如:key:value 值,數組用來存儲 key 經過哈希函數計算后的值與數組長度取余后的值,鏈表存儲 key:value 值。
如下圖:
上圖為什么是 2 個 key:val 在一起?
其實這就是 hash 沖突了,用鏈地址表來解決哈希沖突的問題。
Redis中的哈希表和字典dict
1. 哈希表各結構定義
哈希表dictht
redis3.0 中的哈希表叫 dictht,dictht 的定義:
// https://github.com/redis/redis/blob/3.0/src/dict.h#L69
/* 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; // 哈希表的數組,數組中每個元素都是指針,指向 dictEntry 結構
unsigned long size; // 哈希表的大小,table 數組的大小
unsigned long sizemask; // 哈希表掩碼,用於計算索引值,等於 size-1
unsigned long used; // 哈希表已有的節點(鍵值對)數量
} dictht;
哈希表節點dictEntry
哈希表節點,有的地方取名為哈希桶 bucket,節點 Node 等等,不過表達意思是一樣的。
上面 redis3.0 哈希表 dictht 里的節點 dictEntry 是怎么定義? 代碼如下:
// https://github.com/redis/redis/blob/3.0/src/dict.h#L47
typedef struct dictEntry {
void *key; // 鍵 key
union { // 值 val
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 指向下一個哈希表節點,鏈表法解決hash沖突
} dictEntry;
key 屬性保存鍵值對中的鍵,v 屬性保存鍵值對中的值,其中這個 v 值可能是一個指針,或者是一個 uint64_t 整數,或者是 int64_t 整數,或是 double 類型浮點數。
dictEnty 表節點和 dictht 哈希表結構關系如下圖:
next:指向下一個哈希節點,用鏈表法來解決哈希沖突。
hash沖突:
上面的 dictEntry 結構里的屬性 next 就是解決這個哈希鍵沖突問題的。
有沖突的值,就用鏈表來記錄下一個值。
哈希算法
Redis 中計算哈希值的哈希函數有好幾個。
-
dictIntHashFunction 計算整型類型哈希值的哈希函數
unsigned int dictIntHashFunction(unsigned int key)
-
dictGenHashFunction MurmurHash2 哈希算法, by Austin Appleby,用於計算字符串的哈希值的哈希函數
unsigned int dictGenHashFunction(const void *key, int len)
-
dictGenCaseHashFunction djb 哈希算法,大小寫敏感的哈希函數
/* And a case insensitive hash function (based on djb hash) */ unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len)
2. 字典dict
字典dict
上面我們已經了解,在 Redis 中用 dictht 來表示哈希表,但是,在使用哈希表時,Redis 又定義了一個字典 dict 的數據結構。
為什么要再定義一個 dict 結構?
- 為了擴展哈希表(rehash)的時候,能夠方面的操作哈希表。為此里面定義了 2 個哈希表 ht[2]。
字典 dict.h/dict 結構定義:
typedef struct dict {
dictType *type; // 指針,指向dictType 結構,dictType 中包含很多自定義函數,見下面
void *privdata; // 私有數據,保存dictType結構中的函數參數
dictht ht[2]; // hash表,ht[2] 表示有2張表
long rehashidx; /* rehashing not in progress if rehashidx == -1 *///rehash 標識,rehashidx=-1,沒進行rehash
int iterators; /* number of iterators currently running */// 正在運行的迭代器數量
} dict;
*type:保存了很多函數,這些函數是操作特定類型鍵值對的函數,Redis 會為用途不同的字典設置不同類型特定函數。
ht[2]:包含 2 個 dictht哈希表,為什么有2張表?rehash 時會用到 ht[1]。一般情況下只使用 ht[0]。
rehashidx:這個屬性與 rehash 有關,記錄 rehash 目前的進度,如果目前沒有進行 rehash,那么 rehashidx=-1。
dict.h/dictType 結構:
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;
字典 dict 圖示:
3. rehash
a. 什么是 rehash ?
- 擴大或縮小哈希表容量。
b. 為什么有 rehash ?
- 當哈希表的數據量持續增長,而哈希表容量大小固定時,就可能會有 2 個或以上數量的鍵被分配到哈希表數組的同一個索引上,於是就發生了沖突(collision)。
- 當然沖突可以用鏈表法(separate chaining)解決,但是為了哈希表的性能,要盡量避免沖突,就要對哈希表進行擴容或縮容。
哈希表中有一個負載因子(load factor)的概念:
負載因子 = 哈希表已保存的鍵值對數量(使用的數量) / 哈希表的長度
load_factor = ht[0].used / ht[0].size
這個負載因子的概念是用來衡量哈希表容量大小情況的。哈希表中的鍵值對數量少,負載因子也小。
當負載因子超過某個闕值時,為了維持哈希的容量在一定合理范圍,就會對哈希表容量進行 resize 操作:
- 擴大哈希表容量
- 縮小哈希表容量
c. 什么時候進行擴容和縮容操作?
-
擴容條件
滿足下面任一條件都會觸發哈希表擴容
-
服務器目前沒有執行 bgsave 命令,或 bgrewriteaof 命令,並且哈希表的負載因子 >=1
-
服務器目前在執行 bgsave 命令,或 bgrewriteaof 命令並且哈希表的負載因子 >5
-
-
縮容條件
- 哈希表的負載因子 < 0.1
d. 怎么操作擴容和縮容?
也就是說擴容和縮容的操作步驟是什么?
-
為字典 ht[1] 分配內存空間,空間大小取決於要執行的操作,以及當前 ht[0] 的鍵值對數量
-
如果是擴容操作,那么 ht[1] 的空間大小等於第一個 ht[0].used * 2 的 2^n(2的n次冪)
-
如果是縮容操作,那么 ht[1] 的空間大小等於第一個 ht[0].used 的 2^n(2的n次冪)
-
-
將 ht[0] 上所有鍵值重新計算哈希值和索引值后存放到 ht[1] 對應位置上
-
當 ht[0] 上所有的鍵值移動到 ht[1] 后,釋放 ht[0],將 ht[1] 變成 ht[0],並在 ht[1] 上新建一個空哈希表
擴容代碼簡析:
// https://github.com/redis/redis/blob/3.0/src/dict.c#L923
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *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. */
// 如果 ht[0] 為空,則創建並初始化ht[0],然后返回
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
/* 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. */
/*當 (ht[0].used/ht[0].size)>=1 並且,
滿足dict_can_resize=1或ht[0].used/ht[0].size>5時,對字典進行擴容*/
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;
}
// https://github.com/redis/redis/blob/3.0/src/dict.c#L58
static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;
// https://github.com/redis/redis/blob/3.0/src/dict.c#L204
/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
dictht n; /* the new hash table 新建一個哈希表*/
unsigned long realsize = _dictNextPower(size); // 計算擴容或縮容新版哈希表大小
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
// 如果哈希表正在rehash或新建哈希表大小小於現已使用的,則返回錯誤
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
/* Rehashing to the same table size is not useful. */
if (realsize == d->ht[0].size) return DICT_ERR;
/* 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) {
d->ht[0] = n;
return DICT_OK;
}
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}
縮容操作:
// https://github.com/redis/redis/blob/3.0/src/dict.c#L192
int dictResize(dict *d)
{
int minimal;
// dict_can_resize 在 https://github.com/redis/redis/blob/3.0/src/dict.c#L58 這里是設置為 1,如果為0就返回,不進行后面操心
// 或者 dictIsRehashig() 真正進行rehash操心,也返回不rehash操作
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used; // 獲得已經使用ht的數量
if (minimal < DICT_HT_INITIAL_SIZE) // 這個最小值不能小於 DICT_HT_INITIAL_SIZE = 4
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal); // 用dictExpand函數調整字典大小
}
// https://github.com/redis/redis/blob/3.0/src/dict.h#L100
/* This is the initial size of every hash table */
#define DICT_HT_INITIAL_SIZE 4