更多精彩文章,關注【ToBeTopJavaer】,更有數萬元精品vip資源免費等你來拿!!!
接下來我們要剖析的基本類型是Hash,相信大家對Hash都不會陌生吧,下面我們將深入源碼剖析Redis中Hash的實現。
首先我們看一張圖:
存儲類型
包含鍵值對的無序散列表。value 只能是字符串,不能嵌套其他類型。
同樣是存儲字符串,Hash 與 String 的主要區別?
1、把所有相關的值聚集到一個 key 中,節省內存空間
2、只使用一個 key,減少 key 衝突
3、當需要批量獲取值的時候,只需要使用一個命令,減少內存/IO/CPU 的消耗
Hash 不適合的場景:
1、Field 不能單獨設置過期時間
2、沒有 bit 操作
3、需要考慮數據量分佈的問題(value 值非常大的時候,無法分佈到多個節點)
操作命令存儲(實現)原理
Redis 的 Hash 本身也是一個 KV 的結構,類似於 Java 中的 HashMap。
外層的哈希(Redis KV 的實現)只用到了 hashtable。當存儲 hash 數據類型時,
我們把它叫做內層的哈希。內層的哈希底層可以使用兩種數據結構實現:
ziplist:OBJ_ENCODING_ZIPLIST(壓縮列表)
hashtable:OBJ_ENCODING_HT(哈希表)
如下圖所示:
問題一、那麼在什麼時候會用到ziplist,什麼時候用到hashtable呢?
在redis.conf我們可以看到:
在源碼中:
/* 源碼位置: t_hash.c , 當達字段個數超過閾值, 使用 HT 作為編碼 */ if (hashTypeLength(o) > server.hash_max_ziplist_entries) hashTypeConvert(o, OBJ_ENCODING_HT); /*源碼位置: t_hash.c, 當字段值長度過大, 轉為 HT */ for (i = start; i <= end; i++) { if (sdsEncodedObject(argv[i]) && sdslen(argv[i]->ptr) > server.hash_max_ziplist_value) { hashTypeConvert(o, OBJ_ENCODING_HT); break; } }複製代碼從而我們可以得知,當 hash 對象同時滿足以下兩個條件的時候,使用 ziplist 編碼:
1)所有的鍵值對的健和值的字符串長度都小於等於 64byte(一個英文字母
一個字節);
2)哈希對象保存的鍵值對數量小於 512 個。
一個哈希對象超過配置的閾值(鍵和值的長度有>64byte,鍵值對個數>512 個)時,
會轉換成哈希表(hashtable)。
問題二、什麼是ziplist壓縮列表
ziplist 壓縮列表
ziplist 壓縮列表是什麼?ziplist 是一個經過特殊編碼的雙向鏈表,它不存儲指向上一個鏈表節點和指向下一
個鏈表節點的指針,而是存儲上一個節點長度和當前節點長度,通過犧牲部分讀寫性能,
來換取高效的內存空間利用率,是一種時間換空間的思想。只用在字段個數少,字段值
小的場景裡面。
ziplist 的內部結構?總體架構如下圖所示:
entry對象定義的源碼如下:
typedef struct zlentry { unsigned int prevrawlensize; /* 上一個鏈表節點佔用的長度 */ unsigned int prevrawlen; /* 存儲上一個鏈表節點的長度數值所需要的字節數 */ unsigned int lensize; /* 存儲當前鏈表節點長度數值所需要的字節數 */ unsigned int len; /* 當前鏈表節點佔用的長度 */ unsigned int headersize; /* 當前鏈表節點的頭部大小(prevrawlensize + lensize),即非數據域的大小 */ unsigned char encoding; /* 編碼方式 */ unsigned char *p; /* 壓縮鏈表以字符串的形式保存,該指針指向當前節點起始位置 */ } zlentry;複製代碼問題三、什麼是hashtable( dict)?
hashtable是什麼?在 Redis 中,hashtable 被稱為字典(dictionary),它是一個數組+鏈表的結構。
前面我們知道了,Redis 的 KV 結構是通過一個 dictEntry 來實現的。
Redis 又對 dictEntry 進行了多層的封裝。
dictEntry 定義如下:
typedef struct dictEntry { void *key; /* key 關鍵字定義 */ union { void *val; uint64_t u64; /* value 定義 */ int64_t s64; double d; } v; struct dictEntry *next; /* 指向下一個鍵值對節點 */ } dictEntry複製代碼dictEntry 放到了 dictht(hashtable 裡面):
/* 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; /* 掩碼大小, 用於計算索引值。 總是等於 size-1 */ unsigned long used; /* 已有節點數 */ } dictht;複製代碼dictht 放到了 dict 裡面:
typedef struct dict { dictType *type; /* 字典類型 */ void *privdata; /* 私有數據 */ dictht ht[2]; /* 一個字典有兩個哈希表 */ long rehashidx; /* rehash 索引 */ unsigned long iterators; /* 當前正在使用的迭代器數量 */ } dict;複製代碼從最底層到最高層 dictEntry——dictht——dict——OBJ_ENCODING_HT
哈希的總體存儲結構如下:
注意: dictht 後面是 NULL 說明第二個 ht 還沒用到。 dictEntry*後面是 NULL 說明沒有 hash 到這個地址。 dictEntry 後面是NULL 說明沒有發生哈希衝突。
問題三、為什麼要定義兩個hash表呢?ht[2]?
redis 的 hash 默認使用的是 ht[0],ht[1]不會初始化和分配空間。
哈希表 dictht 是用鏈地址法來解決碰撞問題的。在這種情況下,哈希表的性能取決於它的大小(size 屬性)和它所保存的節點的數量(used 屬性)之間的比率:
1. 比率在 1:1 時(一個哈希表 ht 只存儲一個節點 entry),哈希表的性能最好;
2. 如果節點數量比哈希表的大小要大很多的話(這個比例用 ratio 表示,5 表示平均一個 ht 存儲 5 個 entry),那麼哈希表就會退化成多個鏈表,哈希表本身的性能優勢就不再存在。
在這種情況下需要擴容。Redis 裡面的這種操作叫做 rehash。
1、為字符 ht[1]哈希表分配空間,這個哈希表的空間大小取決於要執行的操作,以及 ht[0]當前包含的鍵值對的數量。
擴展:ht[1]的大小為第一個大於等於 ht[0].used*2。
2、將所有的 ht[0]上的節點 rehash 到 ht[1]上,重新計算 hash 值和索引,然後放入指定的位置。
3、當 ht[0]全部遷移到了 ht[1]之後,釋放 ht[0]的空間,將 ht[1]設置為 ht[0]表,並創建新的 ht[1],為下次 rehash 做準備。
問題四、什麼時候觸發擴容?
關鍵因素:負載因子
定義源碼如下:
static int dict_can_resize = 1; static unsigned int dict_force_resize_ratio = 5;複製代碼ratio = used / size,已使用節點與字典大小的比例。
dict_can_resize 為 1 並且 dict_force_resize_ratio 已使用節點數和字典大小之間的比率超過 1:5,觸發擴容。
擴容判斷 _dictExpandIfNeeded源碼如下:
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); }r eturn DICT_OK;複製代碼擴容方法 dictExpand源碼如下:
static int dictExpand(dict *ht, unsigned long size) { dict n; /* the new hashtable */ unsigned long realsize = _dictNextPower(size), i; /* the size is invalid if it is smaller than the number of * elements already inside the hashtable */ if (ht->used > size) return DICT_ERR; _dictInit(&n, ht->type, ht->privdata); n.size = realsize; n.sizemask = realsize-1; n.table = calloc(realsize,sizeof(dictEntry*)); /* Copy all the elements from the old to the new table: * note that if the old hash table is empty ht->size is zero, * so dictExpand just creates an hash table. */ n.used = ht->used; for (i = 0; i < ht->size && ht->used > 0; i++) { dictEntry *he, *nextHe; if (ht->table[i] == NULL) continue; /* For each hash entry on this slot... */ he = ht->table[i]; while(he) { unsigned int h; nextHe = he->next; /* Get the new element index */ h = dictHashKey(ht, he->key) & n.sizemask; he->next = n.table[h]; n.table[h] = he; ht->used--; /* Pass to the next element */ he = nextHe; } }a ssert(ht->used == 0); free(ht->table); /* Remap the new hashtable in the old */ *ht = n; return DICT_OK; }複製代碼縮容源碼如下:
int htNeedsResize(dict *dict) { long long size, used; size = dictSlots(dict); used = dictSize(dict); return (size > DICT_HT_INITIAL_SIZE &&(used*100/size < HASHTABLE_MIN_FILL)); }複製代碼應用場景
String
String 可以做的事情,Hash 都可以做。
存儲對象類型的數據
比如對象或者一張表的數據,比 String 節省了更多 key 的空間,也更加便於集中管理。
購物車
key:用戶 id;
field:商品 id;
value:商品數量。
+1:hincr。
-1:hdecr。
刪除:hdel。
全選:hgetall。
商品數:hlen。
今天我們從底層源碼剖析了基本數據類型Hash,接下來我們將會對剩下的幾個常用的基本類型的深入探討,敬請期待。
更多精彩文章,關注【ToBeTopJavaer】,更有數萬元精品vip資源免費等你來拿!!!歡迎關注,會陸續發佈一些知識點總結,減少你的讀書時間,一起交流面試經驗!每月隨機抽取20名粉絲進入高級技術交流群(大量資料、BAT員工)!
div class="pgc-img">閱讀更多 GetJob 的文章