Redis的存储(实现)原理
Redis 的Hash 本身也是一個KV 的結(jié)構(gòu),類似于Java 中的HashMap。
外層的哈希(Redis KV 的實現(xiàn))只用到了hashtable。當(dāng)存儲hash 數(shù)據(jù)類型時,我們把它叫做內(nèi)層的哈希。內(nèi)層的哈希底層可以使用兩種數(shù)據(jù)結(jié)構(gòu)實現(xiàn):
ziplist:OBJ_ENCODING_ZIPLIST(壓縮列表)
hashtable:OBJ_ENCODING_HT(哈希表)
127.0.0.1:6379> hset h2 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (integer) 1 127.0.0.1:6379> hset h3 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (integer) 1 127.0.0.1:6379> object encoding h2 "ziplist" 127.0.0.1:6379> object encoding h3 "hashtable"ziplist 壓縮列表
ziplist 壓縮列表是什么?
/* ziplist.c 源碼頭部注釋*/
The ziplist is a specially encoded dually linked list that is designed to be very memory efficient. It stores both strings and
integer values, where integers are encoded as actual integers instead of a series of characters. It allows push and pop
operations on either side of the list in O(1) time. However, because every operation requires a reallocation of the memory
used by the ziplist, the actual complexity is related to the amount of memory used by the ziplist.
ziplist 是一個經(jīng)過特殊編碼的雙向鏈表,它不存儲指向上一個鏈表節(jié)點和指向下一個鏈表節(jié)點的指針,而是存儲上一個節(jié)點長度和當(dāng)前節(jié)點長度,通過犧牲部分讀寫性能,來換取高效的內(nèi)存空間利用率,是一種時間換空間的思想。只用在字段個數(shù)少,字段值小的場景里面。
?
ziplist 的內(nèi)部結(jié)構(gòu)?
ziplist.c 源碼第16 行的注釋:
* <zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
typedef struct zlentry {unsigned int prevrawlensize; /* 上一個鏈表節(jié)點占用的長度*/unsigned int prevrawlen; /* 存儲上一個鏈表節(jié)點的長度數(shù)值所需要的字節(jié)數(shù)*/unsigned int lensize; /* 存儲當(dāng)前鏈表節(jié)點長度數(shù)值所需要的字節(jié)數(shù)*/unsigned int len; /* 當(dāng)前鏈表節(jié)點占用的長度*/unsigned int headersize; /* 當(dāng)前鏈表節(jié)點的頭部大小(prevrawlensize + lensize),即非數(shù)據(jù)域的大小*/unsigned char encoding; /* 編碼方式*/unsigned char *p; /* 壓縮鏈表以字符串的形式保存,該指針指向當(dāng)前節(jié)點起始位置*/ } zlentry;編碼encoding(ziplist.c 源碼第204 行)
#define ZIP_STR_06B (0 << 6) //長度小于等于63 字節(jié)
#define ZIP_STR_14B (1 << 6) //長度小于等于16383 字節(jié)
#define ZIP_STR_32B (2 << 6) //長度小于等于4294967295 字節(jié)
?
問題:什么時候使用ziplist 存儲?
當(dāng)hash 對象同時滿足以下兩個條件的時候,使用ziplist 編碼:
1)所有的鍵值對的健和值的字符串長度都小于等于64byte(一個英文字母一個字節(jié));
2)哈希對象保存的鍵值對數(shù)量小于512 個。
/* src/redis.conf 配置*/
hash-max-ziplist-value 64 // ziplist 中最大能存放的值長度 hash-max-ziplist-entries 512 // ziplist 中最多能存放的entry 節(jié)點數(shù)量 /* 源碼位置:t_hash.c ,當(dāng)達字段個數(shù)超過閾值,使用HT 作為編碼*/ if (hashTypeLength(o) > server.hash_max_ziplist_entries) hashTypeConvert(o, OBJ_ENCODING_HT); /*源碼位置: t_hash.c,當(dāng)字段值長度過大,轉(zhuǎn)為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;} }一個哈希對象超過配置的閾值(鍵和值的長度有>64byte,鍵值對個數(shù)>512 個)時,會轉(zhuǎn)換成哈希表(hashtable)。
?
hashtable(dict)
在Redis 中,hashtable 被稱為字典(dictionary),它是一個數(shù)組+鏈表的結(jié)構(gòu)。
源碼位置:dict.h
前面我們知道了,Redis 的KV 結(jié)構(gòu)是通過一個dictEntry 來實現(xiàn)的。
Redis 又對dictEntry 進行了多層的封裝。
typedef struct dictEntry {void *key; /* key 關(guān)鍵字定義*/union {void *val; uint64_t u64; /* value 定義*/int64_t s64; double d;} v;struct dictEntry *next; /* 指向下一個鍵值對節(jié)點*/ } 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; /* 哈希表數(shù)組*/unsigned long size; /* 哈希表大小*/unsigned long sizemask; /* 掩碼大小,用于計算索引值。總是等于size-1 */unsigned long used; /* 已有節(jié)點數(shù)*/ } dictht;ht 放到了dict 里面:
typedef struct dict {dictType *type; /* 字典類型*/void *privdata; /* 私有數(shù)據(jù)*/dictht ht[2]; /* 一個字典有兩個哈希表*/long rehashidx; /* rehash 索引*/unsigned long iterators; /* 當(dāng)前正在使用的迭代器數(shù)量*/ } dict;從最底層到最高層dictEntry——dictht——dict——OBJ_ENCODING_HT
總結(jié):哈希的存儲結(jié)構(gòu)
注意:dictht 后面是NULL 說明第二個ht 還沒用到。dictEntry*后面是NULL 說明沒有hash 到這個地址。dictEntry 后面是NULL 說明沒有發(fā)生哈希沖突。
問題:為什么要定義兩個哈希表呢?ht[2]
redis 的hash 默認使用的是ht[0],ht[1]不會初始化和分配空間。
哈希表dictht 是用鏈地址法來解決碰撞問題的。在這種情況下,哈希表的性能取決于它的大小(size 屬性)和它所保存的節(jié)點的數(shù)量(used 屬性)之間的比率:
比率在1:1 時(一個哈希表ht 只存儲一個節(jié)點entry),哈希表的性能最好;
如果節(jié)點數(shù)量比哈希表的大小要大很多的話(這個比例用ratio 表示,5 表示平均一個ht 存儲5 個entry),那么哈希表就會退化成多個鏈表,哈希表本身的性能優(yōu)勢就不再存在。
在這種情況下需要擴容。Redis 里面的這種操作叫做rehash。
rehash 的步驟:
1、為字符ht[1]哈希表分配空間,這個哈希表的空間大小取決于要執(zhí)行的操作,以及ht[0]當(dāng)前包含的鍵值對的數(shù)量。
擴展:ht[1]的大小為第一個大于等于ht[0].used*2。
2、將所有的ht[0]上的節(jié)點rehash 到ht[1]上,重新計算hash 值和索引,然后放入指定的位置。
3、當(dāng)ht[0]全部遷移到了ht[1]之后,釋放ht[0]的空間,將ht[1]設(shè)置為ht[0]表,并創(chuàng)建新的ht[1],為下次rehash 做準(zhǔn)備。
?
問題:什么時候觸發(fā)擴容?
負載因子(源碼位置:dict.c):
static int dict_can_resize = 1; static unsigned int dict_force_resize_ratio = 5;ratio = used / size,已使用節(jié)點與字典大小的比例
dict_can_resize 為1 并且dict_force_resize_ratio 已使用節(jié)點數(shù)和字典大小之間的比率超過1:5,觸發(fā)擴容
擴容判斷_dictExpandIfNeeded(源碼dict.c)
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;擴容方法dictExpand(源碼dict.c)
?
縮容:server.c
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)); }?
總結(jié)
以上是生活随笔為你收集整理的Redis的存储(实现)原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis Hash 哈希 结构
- 下一篇: Redis中的List 列表