認識Redis高性能背後的數據結構(一)

前言

Redis做爲一個不少大廠用來解決併發和快速響應的利器,極高的性能讓它獲得不少公司的青睞,我認爲Redis的高性能和其底層的數據結構的設計和實現是分不開的。使用過Redis的同窗可能都知道Redis有五種基本的數據類型:string、list、hash、set、zset;這些只是Redis服務對於客戶端提供的第一層面的數據結構。其實內部的數據結構仍是有第二個層面的實現,Redis利用第二個層面的一種或多種數據類型來實現了第一層面的數據類型。我想有和我同樣對Redis底層數據結構感興趣的人,那咱們就一塊兒來研究一下Redis高性能的背後的實現底層數據結構的設計和實現。html

這裏咱們主要研究的是第二層面的數據結構的實現,其Redis中五種基本的數據類型都是經過如下數據結構實現的,咱們接下來一個一個來看:node

  • sds
  • ziplist
  • quicklist
  • dict
  • skiplist

1. 動態字符串(SDS)

String類型無論是在什麼編程語言中都是最多見和經常使用的數據類型,Redis底層是使用C語言編寫的,可是Redis沒有使用C語言字符串類型,而是自定義了一個Simple Dynamic String (簡稱SDS)做爲Redis底層String的實現,其SDS相比於C語言的字符串有如下優點:git

  • 可動態擴展內存。sds表示的字符串是能夠動態擴容的。由於C語言字符串不記錄自身的長度,若是改動字符串長度,那就須要從新爲新的字符串分配內存,若是不分配內存,可能會產生溢出。可是SDS不須要手動修改內存大小,也不會出現緩衝區溢出問題,由於SDS自己會記錄存儲的數據大小以及最大的容量,當超過了容量會自動擴容。
  • 二進制安全(Binary Safe)。sds能存儲任意二進制數據,不只僅能夠存儲字符串,還能存儲音頻、圖片、壓縮文件等二進制數據。 SDS的api都會以處理二進制的方式來處理存放在buf數組裏面,不會對數據作任何限制。
  • 除此以外,sds還兼容了C語言的字符類型。

下面是Redis中SDS的部分源碼。sds源碼github

typedef char *sds;
/* Note: sdshdr5 is never used, we just access the flags byte directly.  * However is here to document the layout of type 5 SDS strings. */ struct __attribute__ ((__packed__)) sdshdr5 {  unsigned char flags; /* 3 lsb of type, and 5 msb of string length */  char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 {  uint8_t len; /* used */  uint8_t alloc; /* excluding the header and null terminator */  unsigned char flags; /* 3 lsb of type, 5 unused bits */  char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 {  uint16_t len; /* used */  uint16_t alloc; /* excluding the header and null terminator */  unsigned char flags; /* 3 lsb of type, 5 unused bits */  char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 {  uint32_t len; /* used */  uint32_t alloc; /* excluding the header and null terminator */  unsigned char flags; /* 3 lsb of type, 5 unused bits */  char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 {  uint64_t len; /* used */  uint64_t alloc; /* excluding the header and null terminator */  unsigned char flags; /* 3 lsb of type, 5 unused bits */  char buf[]; }; 複製代碼

其實咱們能夠經過源碼發現sds的內部組成,咱們發現sds被定義成了char類型,難道Redis的String類型底層就是char嗎?其實sds爲了和傳統的C語言字符串保持類型兼容,因此它們的類型定義是同樣的,都是char *,可是sds不等同是char。web

真正存儲數據的是在sdshdr中的buf中,這個數據結構除了能存儲字符串之外,還能夠存儲像圖片,視頻等二進制數據,。SDS爲了兼容C語言的字符串,遵循了C語言字符串以空字符結尾的慣例, 因此在buf中, 用戶數據後總跟着一個\0. 即圖中 "數據" + "\0" 是爲所謂的buf。另外注意sdshdr有五種類型,其實sdshdr5是不使用的,其實使用的也就四種。定義這麼多的類型頭是爲了能讓不一樣長度的字符串可使用不一樣大小的header。這樣短字符串就能使用較小的 header,從而節省內存。redis

SDS概覽以下圖:算法

UTOOLS1591340532574.png
UTOOLS1591340532574.png

除了sdshdr5以外,其它4個header的結構都包含3個字段數據庫

  1. len: 表示字符串的真正長度(不包含 \0結束符在內)。
  2. alloc: 表示整個SDS最大容量(不包含 \0字節)。
  3. flags: 老是佔用一個字節。其中的最低3個bit用來表示header的類型。header的類型共有5種,用到的也就4種,在sds.h中有常量定義。

2. 列表 list

2.1 底層數據結構

Redis對外暴露的是list數據類型,它底層實現所依賴的內部數據結構其實有幾種,在Redis3.2版本以前,鏈表的底層實現是linkedListzipList,可是在版本3.2以後 linkedListzipList就基本上被棄用了,使用quickList來做爲鏈表的底層實現,ziplist雖然被被quicklist替代,可是ziplist仍然是hash和zset底層實現之一。編程

2.2 壓縮鏈表 zipList 轉 雙向鏈表 linkedList

這裏咱們使用Redis2.8版本能夠看出來,當我插入鍵 k5 中 110條比較短的數據時候,列表是ziplist編碼,當我再往裏面插入10000條數據的時候,k5的數據編碼就變成了linkedlist。api

UTOOLS1591706020432.png
UTOOLS1591706020432.png

Redis3.2版本以前,list底層默認使用的zipList做爲列表底層默認數據結,在必定的條件下,zipList 會轉成 linkedList。Redis之因此這樣設計,由於雙向鏈表佔用的內存比壓縮列表要多, 因此當建立新的列表鍵時, 列表會優先考慮使用壓縮列表, 而且在有須要的時候, 才從壓縮列表實現轉換到雙向鏈表實現。在什麼狀況下zipList會轉成 linkedList,須要知足一下兩個任意條件:

  • 這個字符串的長度超過 server.list_max_ziplist_value (默認值爲 64 )。
  • ziplist 包含的節點超過 server.list_max_ziplist_entries (默認值爲 512 )。

這兩個條件是能夠修改的,在 redis.conf 中:

list-max-ziplist-value 64 
list-max-ziplist-entries 512 
複製代碼

注意:這裏列表list的這個配置,只有在Redis3.2版本以前的配置中才能找到,由於Redis3.2和3.2之後的版本去掉了這個配置,由於底層實現不在使用ziplist,而是採用quicklist來做爲默認的實現。

2.3 雙向鏈表 linedList

當鏈表entry數據超過5十二、或單個value 長度超過64,底層就會將zipList轉化成linkedlist編碼,linkedlist是標準的雙向鏈表,Node節點包含prev和next指針,能夠進行雙向遍歷;還保存了 head 和 tail 兩個指針。所以,對鏈表的表頭和表尾進行插入的時間複雜度都爲O (1) , 這是也是高效實現 LPUSH 、 RPOP、 RPOPLPUSH 等命令的關鍵。

2.4 壓縮列表 zipList

雖然Redis3.2版本之後再也不直接使用ziplist來實現列表建,可是底層仍是間接的利用了ziplist來實現的。

壓縮列表是Redis爲了節省內存而開發的,Redis官方對於ziplist的定義是(出自Redis源碼中src/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是一個通過特殊編碼的雙向鏈表,它的設計目標就是爲了提升存儲效率。ziplist能夠用於存儲字符串或整數,其中整數是按真正的二進制表示進行編碼的,而不是編碼成字符串序列。它能以O(1)的時間複雜度在表的兩端提供pushpop操做。

ziplist 將列表中每一項存放在先後連續的地址空間內,每一項因佔用的空間不一樣,而採用變長編碼。當元素個數較少時,Redis 用 ziplist 來存儲數據,當元素個數超過某個值時,鏈表鍵中會把 ziplist 轉化爲 linkedlist,字典鍵中會把 ziplist 轉化爲 hashtable。因爲內存是連續分配的,因此遍歷速度很快。

2.4.1 壓縮列表的數據結構

ziplist 是一個特殊的雙向鏈表,ziplist沒有維護雙向指針:prev next;而是存儲上一個 entry的長度和 當前entry的長度,經過長度推算下一個元素在什麼地方,犧牲讀取的性能,得到高效的存儲空間,這是典型的"時間換空間"。

ziplist使用連續的內存塊,每個節點(entry)都是連續存儲的;ziplist 存儲分佈以下:

UTOOLS1591627618164.png
UTOOLS1591627618164.png

每一個字段表明的含義。

  • zlbytes: 32bit,表示ziplist佔用的字節總數(也包括 zlbytes自己佔用的4個字節)。
  • zltail: 32bit,表示ziplist表中最後一項(entry)在ziplist中的偏移字節數。 zltail的存在,使得咱們能夠很方便地找到最後一項(不用遍歷整個ziplist),從而能夠在ziplist尾端快速地執行push或pop操做。
  • zllen: 16bit, 表示ziplist中數據項(entry)的個數。zllen字段由於只有16bit,因此能夠表達的最大值爲2^16-1。這裏須要特別注意的是,若是ziplist中數據項個數超過了16bit能表達的最大值,ziplist仍然能夠來表示。那怎麼表示呢?這裏作了這樣的規定:若是 zllen小於等於2^16-2(也就是不等於2^16-1),那麼 zllen就表示ziplist中數據項的個數;不然,也就是 zllen等於16bit全爲1的狀況,那麼 zllen就不表示數據項個數了,這時候要想知道ziplist中數據項總數,那麼必須對ziplist從頭至尾遍歷各個數據項,才能計數出來。
  • entry: 表示真正存放數據的數據項,長度不定。一個數據項(entry)也有它本身的內部結構。
  • zlend: ziplist最後1個字節,是一個結束標記,值固定等於255。

2.4.2 zipList節點entry結構

ziplist每個存儲節點、都是一個 zlentry。zlentry的源碼在ziplist.c 第 268行

/* We use this function to receive information about a ziplist entry.  * Note that this is not how the data is actually encoded, is just what we  * get filled by a function in order to operate more easily. */ typedef struct zlentry {  unsigned int prevrawlensize; /* prevrawlensize是指prevrawlen的大小,有1字節和5字節兩種*/  unsigned int prevrawlen; /* 前一個節點的長度 */  unsigned int lensize; /* lensize爲編碼len所需的字節大小*/  unsigned int len; /* len爲當前節點長度*/  unsigned int headersize; /* 當前節點的header大小 */  unsigned char encoding; /*節點的編碼方式:ZIP_STR_* or ZIP_INT_* */  unsigned char *p; /* 指向節點的指針 */ } zlentry; 複製代碼
  • prevrawlen:記錄前一個節點所佔有的內存字節數,經過該值,咱們能夠從當前節點計算前一個節點的地址,能夠用來實現表尾向表頭節點遍歷;prevrawlen是變長編碼,有兩種表示方法
    • 若是前一節點的長度小於 254 字節,則使用1字節(uint8_t)來存儲prevrawlen;
    • 若是前一節點的長度大於等於 254 字節,那麼將第 1 個字節的值設爲 254 ,而後用接下來的 4 個字節保存實際長度。
  • len/encoding:記錄了當前節點content佔有的內存字節數及其存儲類型,用來解析content用;
  • content:保存了當前節點的值。 最關鍵的是prevrawlen和len/encoding,content只是實際存儲數值的比特位。

2.4.3 爲何zipList 能夠作到數據壓縮

由於ziplist採用了一段連續的內存來存儲數據,減小了內存碎片和指針的內存佔用。其次表中每一項存放在先後連續的地址空間內,每一項因佔用的空間不一樣,而採用變長編碼,並且當節點較少時,ziplist更容易被加載到CPU緩存中。這也是ziplist能夠作到壓縮內存的緣由。

2.4.4 爲何zipList被捨棄了

經過上面咱們已經清楚的瞭解的ziplist的數據結構,在ziplist中每一個zlentry都存儲着前一個節點所佔的字節數,而這個數值又是變長的,這樣的數據結構可能會引發ziplist的連鎖更新。假設咱們有一個壓縮鏈表 entry1 entry2 entry3 .......,entry1的長度正好是 253個字節,那麼按照咱們上面所說的,entry2.prevrawlen 記錄了entry1的長度,使用1個字節來保存entry1的大小,假如如今在entry1 和 entry2之間插入了一個新的 new_entry節點,而new_entry的大小正好是254,那此時entry2.prevrawlen就須要擴充爲5字節;若是entry2的總體長度變化又引發了entry3.prevrawlen的存儲長度變化,如此連鎖的更新直到尾結點或者某一個節點的prevrawlen足以存放以前節點的長度,固然刪除節點也是一樣的道理,只要咱們的操做的節點以後的prevrawlen發生了改變就會出現這種連鎖更新。

因爲ziplist連鎖更新的問題,也使得ziplist的優缺點極其明顯;ziplist被設計出來的目的是節省內存,這種結構並不擅長作修改操做。一旦數據發生改動,就會引起內存從新分配,可能致使內存拷貝。也使得後續Redis採起折中,利用quicklist替換了ziplist。

2.5 快速列表 quickList

基於上面所說,咱們已經知道了ziplist的缺陷,因此在Redis3.2版本之後,列表的底層默認實現就使用了quicklist來代替ziplist和linkedlist?接下來咱們就看一下quicklist的數據結構是什麼樣的,爲何使用quicklist做爲Redis列表的底層實現,它的優點相比於ziplist優點在哪裏,接下來咱們就一塊兒來看一下quicklist的具體實現。下面是我基於Redis3.2的版本作的操做,這裏咱們能夠看到列表的底層默認的實現是quicklist對象編碼。

UTOOLS1591706621114.png
UTOOLS1591706621114.png

2.5.1 quicklist數據結構

quicklist總體的數據結構以下:

UTOOLS1591872656750.png

quicklist源碼 redis/src/quicklist.h結構定義以下:

typedef struct quicklist {
 quicklistNode *head; // 頭結點  quicklistNode *tail; // 尾結點   unsigned long count; // 全部ziplist數據項的個數總和  unsigned long len; //quicklistNode的節點個數  int fill : QL_FILL_BITS; //ziplist大小設置,經過配置文件中list-max-ziplist-size參數設置的值。  unsigned int compress : QL_COMP_BITS; //節點壓縮深度設置,經過配置文件list-compress-depth參數設置的值。  unsigned int bookmark_count: QL_BM_BITS;  quicklistBookmark bookmarks[]; } quicklist; 複製代碼

其實就算使用的quicklist結構來代替ziplist,那quicklist也是有必定的缺點,底層仍然使用了ziplist,這樣一樣會有一個問題,由於ziplist是一個連續的內存地址,若是ziplist過小,就會產生不少小的磁盤碎片,從而下降存儲效率,若是ziplist很大,那分配連續的大塊內存空間的難度也就越大,也會下降存儲的效率。如何平衡ziplist的大小呢?那這樣就會取決於使用的場景,Redis提供了一個配置參數list-max-ziplist-size能夠調整ziplist的大小。

當取正值的時候,表示按照數據項個數來限定每一個quicklist節點上的ziplist長度。好比,當這個參數配置成5的時候,表示每一個quicklist節點的ziplist最多包含5個數據項。當取負值的時候,表示按照佔用字節數來限定每一個quicklist節點上的ziplist長度。這時,它只能取-1到-5這五個值,每一個值含義以下:

  • -5: 每一個quicklist節點上的ziplist大小不能超過64 Kb。(注:1kb => 1024 bytes)
  • -4: 每一個quicklist節點上的ziplist大小不能超過32 Kb。
  • -3: 每一個quicklist節點上的ziplist大小不能超過16 Kb。
  • -2: 每一個quicklist節點上的ziplist大小不能超過8 Kb。(-2是Redis給出的默認值)
  • -1: 每一個quicklist節點上的ziplist大小不能超過4 Kb。

當咱們數據量很大的時候,最方便訪問的數據基本上就是隊列頭和隊尾的數據(時間複雜度爲O(1)),中間的數據被訪問的頻率比較低(訪問性能也比較低,時間複雜度是O(N),若是你的使用場景符合這個特色,Redis爲了壓縮內存的使用,提供了list-compress-depth這個配置可以把中間的數據節點進行壓縮。quicklist內部節點的壓縮算法,採用的LZF——一種無損壓縮算法。

這個參數表示一個quicklist兩端不被壓縮的節點個數。參數list-compress-depth的取值含義以下:

  • 0: 是個特殊值,表示都不壓縮。這是Redis的默認值。
  • 1: 表示quicklist兩端各有1個節點不壓縮,中間的節點壓縮。
  • 2: 表示quicklist兩端各有2個節點不壓縮,中間的節點壓縮。
  • 3: 表示quicklist兩端各有3個節點不壓縮,中間的節點壓縮。
  • 依此類推

2.5.2 quicklistNode結構

quicklist是由一個個quicklistNode的雙向鏈表構成。

typedef struct quicklistNode {
 struct quicklistNode *prev;  struct quicklistNode *next;  unsigned char *zl;  unsigned int sz; /* ziplist size in bytes */  unsigned int count : 16; /* count of items in ziplist */  unsigned int encoding : 2; /* RAW==1 or LZF==2 */  unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */  unsigned int recompress : 1; /* was this node previous compressed? */  unsigned int attempted_compress : 1; /* node can't compress; too small */  unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode;  typedef struct quicklistLZF {  unsigned int sz;  char compressed[]; } quicklistLZF;  複製代碼

quicklist中的每一個節點都是一個quicklistNode,其中各個字段的含義以下:

  • prev: 指向鏈表前一個節點的指針。
  • next: 指向鏈表後一個節點的指針。
  • zl:數據指針。若是當前節點的數據沒有壓縮,那麼它指向一個ziplist結構;不然,它指向一個quicklistLZF結構。
  • sz:表示zl指向的ziplist的總大小,須要注意的是:若是ziplist被壓縮了,那麼這個sz的值仍然是壓縮前的ziplist大小。
  • count: 表示ziplist裏面包含的數據項個數。
  • encoding:表示ziplist是否壓縮了(以及用了哪一個壓縮算法)。目前只有兩種取值:2表示被壓縮了(並且用的是 LZF壓縮算法),1表示沒有壓縮。
  • container:在目前的實現中,這個值是一個固定的值2,表示使用ziplist做爲數據容器。
  • recompress: 當咱們使用相似lindex這樣的命令查看了某一項壓縮以前的數據時,須要把數據暫時解壓,這時就設置recompress = 1作一個標記,等有機會再把數據從新壓縮。
  • attempted_compress:這不太理解其含義。
  • extra:擴展預留字段。

quicklist結構結合ziplist和linkedlist的優勢,quicklist權衡了時間和空間的消耗,很大程度的優化了性能,quicklist由於隊頭和隊尾操做的時間複雜度都是O(1),因此Redis的列表也能夠被做用隊列來使用。

3. 字典 dict

UTOOLS1592647523856.png
UTOOLS1592647523856.png

經過上圖咱們可以看到hash鍵的底層默認實現的數據結構是ziplist,隨着hash鍵的數量變大時,數據結構就變成了hashtable,雖然這裏的咱們看到的對象編碼格式hashtable,可是Redis底層是使用字典dict來完成了Hash鍵的底層數據結構,不過字典dict的底層實現是使用哈希表來實現的。Redis服務對於客戶端來講,對外暴露的類型是hash,其底層的數據結構實現有兩種,一種是壓縮列表(ziplist),另一種則是字典(dict);關於ziplist的,咱們在說鏈表(list)的時候已經說過了,這裏不重複去說了。咱們這裏就着重的去看一下字典(dict)的具體實現。

這裏仍是要說一下什麼狀況下會從ziplist轉成hashtable呢?redis.conf中提供了兩個參數

hash-max-ziplist-entries 512
hash-max-ziplist-value 64 複製代碼
  • 表示當hash鍵中key所對應的項(field,value)數>512 時候轉爲字典。
  • 表示當hash鍵中key所對應的value長度超過64的時候轉爲字典

3.1 字典(dict)的實現

字典算是Redis比較重要的一個數據結構了,Redis數據庫自己就能夠當作是一個大的字典,Redis之因此會有很高的查詢效率,其實和Redis底層使用的數據類型是有關係的,一般字典的實現會用哈希表做爲底層的存儲,redis的字典實現也是基於時間複雜度爲O(1)的hash算法。

Redis源碼其結構定義以下:dict源碼定義

typedef struct dictEntry {
 void *key;  union {  void *val;  uint64_t u64;  int64_t s64;  double d;  } v;  struct dictEntry *next; } dictEntry;  typedef struct dictType {  uint64_t (*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;  /* 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;  unsigned long used; } dictht;  typedef struct dict {  dictType *type;  void *privdata;  dictht ht[2];  long rehashidx; /* rehashing not in progress if rehashidx == -1 */  unsigned long iterators; /* number of iterators currently running */ } dict; 複製代碼

下圖能更清晰的展現dict的數據結構。

UTOOLS1593064857861.png
UTOOLS1593064857861.png

經過上圖和源碼咱們能夠很清晰的看到,一個字典dict的構成由下面幾項構成:

  1. dictType: 一個指向dictType結構的指針(type),經過自定義的方式使得dict的key和value可以存儲任何類型的數據。
  2. privdate: 一個私的指針,由調用者在建立dict的時候傳進來。
  3. dictht[2]: 一個hash表數組,且數組的大小是2。只有在重哈希的過程當中,ht[0]和ht[1]才都有效。而在日常狀況下,只有ht[0]有效,ht[1]裏面沒有任何數據。
  4. rehashidex: 當前重哈希索引(rehashidx)。若是rehashidx = -1,表示當前沒有在重哈希過程當中;不然,表示當前正在進行重哈希,且它的值記錄了當前重哈希進行到哪一步了。
  5. iterators: 當前正在進行遍歷的iterator的個數。

這裏最重要的仍是dictht這個結構,dictht定義了一個哈希表,其結構由如下組成:

  1. dictEntry:一個dictEntry指針數組(table)。key的哈希值最終映射到這個數組的某個位置上(對應一個bucket)。若是多個key映射到同一個位置,就發生了衝突,那麼就拉出一個dictEntry鏈表。熟悉Java的同窗看到這裏可能會想到HashMap,這裏其實和HashMap的實現有些相像。
  2. size:標識dictEntry指針數組的長度。它老是2的指數。
  3. sizemask:用於將哈希值映射到table的位置索引。它的值等於(size-1),也就是數組的下標。這裏其實和HashMap中計算索引的方法是同樣的。
  4. used:記錄dict中現有的數據個數。它與size的比值就是裝載因子(load factor)。這個比值越大,哈希值衝突機率越高。

3.2 Redis中dict如何進行rehash的

總體看下來,有點相似於Java中HashMap的實現,在處理哈希衝突和數組的大小都是和Java中的HashMap是同樣的,可是這裏有一點不同就是關於擴容的機制,Redis這裏利用了兩個哈希表,另一個哈希表就是擴容用的。Redis中的字典和Java中的HashMap同樣,爲了保證隨着數據量增大致使查詢的效率問題,要適當的調整數組的大小,也就是rehash,也就是咱們熟知擴容。咱們這裏不說Java中的HashMap的擴容了,這裏主要看一下Redis中對於字典的擴容。

那麼何時纔會rehash呢?條件: 1. 服務器目前沒有執行的BGSAVE命令或者BGREWRUTEAOF命令,而且哈希表的負載因子大於等於1; 2. 服務器目前正在執行BGSAVE命令或者BGREWRUTEAOF命令,而且哈希表的負載因子大於等於5;

那究竟是如何進行rehash的,根據上面源碼和數據結構圖能夠看到,字典中定義一個大小爲2的哈希表數組,前面咱們也說到了,在不進行擴容的時候,全部的數據都是存儲在第一個哈希表中,只有在進行擴容的時候纔會用到第二個哈希表。當須要進行rehash的時候,將dictht[1]的哈希表大小設置爲須要擴容以後的大小,而後將dictht[0]中的全部數據從新rehash到dictht[1]中;並且Redis爲了保證在數據量很大的狀況rehash不太過消耗服務器性能,其採用了漸進式rehash,當數據量很小的時候咱們一次性的將數據從新rehash到擴容以後的哈希表中,對Redis服務的性能是能夠忽略不計的,可是當Redis中hash鍵的數量很大,幾十萬甚至上百萬的數據時,這樣rehash對Redis帶來的影響是巨大的,甚至會致使一段時間內Redis中止服務,這是不能接受的。

Redis服務在須要rehash的時候,不是一次性將dictht[0]中的數據所有rehash到dictht[1]中,而是分批進行依次將數據從新rehash到dictht[1]的哈希表中。這就是採用了分治的思想,就算在數據量很大的時候也能避免集中式rehash帶來的巨大計算量。當進行rehash的期間,對字典的增刪改查都會操做兩個哈希表,由於在進行rehahs的時候,兩個哈希表都有說句,當咱們在一個哈希表中查找不到數據的時候,也會去另外一個哈希表查數據。在rehash期間的新增,不會在第一個哈希表中新增,會直接把新增的數據保存到第二個哈希表中這樣能夠確保第一個哈希表中的數據只減不增,直到數據爲空結束rehash。

熟悉Java的同窗可能會想起HashMap中擴容算法,其實包括從容量的設計上和內部的結構都有不少類似的地方,有興趣的同窗能夠去了解一下,也能夠參考我寫的這篇文章《Java1.8中HashMap的騷操做》,相比於Redis中的字典的rehash的方式,我更喜歡的是Java中對於HashMap中精妙的rehahs的方式,其思想仍是很是值得咱們去借鑑的。

相關文章
相關標籤/搜索