1. 程式人生 > >Redis關鍵點(rehash)

Redis關鍵點(rehash)

是一種高效的資料結構,被廣泛的用在key-value儲存中,Redis的dict其實就是一個典型的hash table實現。

是在hash table的大小不能滿足需求,造成過多hash碰撞後需要進行的擴容hash table的操作,其實通常的做法確實是建立一個額外的hash table,將原來的hash table中的資料在新的資料中進行重新輸入,從而生成新的hash表。

  • lazy rehashing:在每次對dict進行操作的時候執行一個slot的rehash
  • active rehashing:每100ms裡面使用1ms時間進行rehash。

dict實現中主要用到如下結構體,其實就是個典型的鏈式hash。

一個dict會有2個hash table,由dictht結構管理,編號為0和1.

使用是優先使用0號hash table,當空間不足時會呼叫dictExpand來擴充套件hash table,此時準備1號hash table用於增量的rehash使用。rehash完成後把0號釋放,1號儲存到0號。

rehashidx是下一個需要rehash的項在ht[0]中的索引,不需要rehash時置為-1。也就是說-1時,表示不進行rehash。

iterators記錄當前dict中的迭代器數,主要是為了避免在有迭代器時rehash,在有迭代器時rehash可能會造成值的丟失或重複,

dictht中的table是一個數組+指標形式的hash表,size表hash陣列(桶)的大小,used表示hash表的元素個數,這兩個值與rehash、resize過程密切相關。sizemask等於size-1,這是為了方便將hash值對映到陣列中。

typedef struct dictEntry {
void *key;
void *val;
struct dictEntry *next;
} dictEntry;
typedef struct dictht {
dictEntry **table;
unsigned long size;//hash桶的個數
unsigned long sizemask;//hash取模的用到
unsigned long used;//元素個數
} dictht;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;
typedef struct dictIterator {
dict *d;
int table;
int index;
dictEntry *entry, *nextEntry;
} dictIterator;

什麼時候dict做擴容

在資料插入的時候會呼叫dictKeyIndex,該方法裡會呼叫_dictExpandIfNeeded,判斷dict是否需要rehash,當dict中元素大於桶的個數時,呼叫dictExpand擴充套件hash

/* Expand the hash table if needed */
 
static int _dictExpandIfNeeded(dict *d)
 
{
 
/* If the hash table is empty expand it to the intial size,
 
* if the table is “full” dobule its size. */
 
if (dictIsRehashing(d)) return DICT_OK;
 
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)
 
return dictExpand(d, ((d->ht[0].size > d->ht[0].used) ?
 
d->ht[0].size : d->ht[0].used)*2);
 
return DICT_OK;
 
}

dictExpand的工作主要是初始化hash表,預設是擴大兩倍(並不單純是桶的兩倍),然後賦值給ht[1],然後狀態改為rehashing,此時該dict開始rehashing

擴容過程如何進行

rehash主要在dictRehash中完成。先看下什麼時候進行rehash。

active rehashing :serverCron中,當沒有後臺子執行緒時,會呼叫incrementallyRehash,最終呼叫dictRehashMilliseconds。incrementallyRehash的時間較長,rehash的個數也比較多。這裡每次執行 1 millisecond rehash 操作;如果未完成 rehash,會在下一個 loop 裡面繼續執行。

/* Rehash for an amount of time between ms milliseconds and ms+1 milliseconds */
 
int dictRehashMilliseconds(dict *d, int ms) {
 
long long start = timeInMilliseconds();
 
int rehashes = 0;
 
while(dictRehash(d,100)) {
 
rehashes += 100;
 
if (timeInMilliseconds()-start > ms) break;
 
}
 
return rehashes;
 
}

lazy rehashing:_dictRehashStep中,也會呼叫dictRehash,而_dictRehashStep每次僅會rehash一個值從ht[0]到 ht[1],但由於_dictRehashStep是被dictGetRandomKey、dictFind、 dictGenericDelete、dictAdd呼叫的,因此在每次dict增刪查改時都會被呼叫,這無疑就加快rehash了過程。

我 們再來看看做rehash的方法。dictRehash每次增量rehash n個元素,由於在自動調整大小時已設定好了ht[1]的大小,因此rehash的主要過程就是遍歷ht[0],取得key,然後將該key按ht[1]的 桶的大小重新rehash,並在rehash完後將ht[0]指向ht[1],然後將ht[1]清空。在這個過程中rehashidx非常重要,它表示上次rehash時在ht[0]的下標位置。

可以看到,redis對dict的rehash是分批進行的,這樣不會阻塞請求,設計的比較優雅。

但是在呼叫dictFind的時候,可能需要對兩張dict表做查詢。唯一的優化判斷是,當key在ht[0]不存在且不在rehashing狀態時,可以速度返回空。如果在rehashing狀態,當在ht[0]沒值的時候,還需要在ht[1]裡查詢。

dictAdd的時候,如果狀態是rehashing,則把值插入到ht[1],否則ht[0]