redis字典實現(xiàn)

一众辨、原理

1.1 數(shù)組+鏈表的底層數(shù)據(jù)結構

字典是用hash實現(xiàn)的煤蚌,通過數(shù)組+鏈表解決hash沖突耕挨。通過hash函數(shù)計算出key的index后,在對應的鏈表上進行數(shù)據(jù)的增刪查尉桩。


字典的hash實現(xiàn)
1.2 漸進式rehash

rehash含義:當dict中的數(shù)據(jù)越來越多時俗孝,數(shù)組中的鏈表會越來越長,dict的增刪改效率越來越低魄健,因此需要對數(shù)組進行擴容赋铝,這就是rehash。
漸進式rehash的含義:當數(shù)組擴容時沽瘦,需要進行一次數(shù)據(jù)遷移革骨。在觸發(fā)擴容時农尖,并不執(zhí)行數(shù)據(jù)遷移,而是在后續(xù)的每次增刪改等操作中良哲,遷移一部分數(shù)據(jù)盛卡。
為什么漸進式rehash:當dict很大時,擴容涉及的數(shù)據(jù)非常的多筑凫,如果一次遷移完會導致服務器卡頓滑沧。

二、數(shù)據(jù)結構定義

2.1 dict
  1. dictType是hash相關的處理函數(shù)巍实,C的多態(tài)實現(xiàn)滓技,在運行時確定需要調用的函數(shù)。
  2. privdata棚潦,在dictType中的處理函數(shù)里會使用該數(shù)據(jù)令漂。
  3. ht[2]。實際的數(shù)據(jù)存儲丸边,非rehash情況下叠必,使用ht[0]存儲數(shù)據(jù),rehash過程中妹窖,ht[0]存老的數(shù)據(jù)纬朝,ht[1]存新的數(shù)據(jù)。
  4. rehashidx骄呼。rehash的進度玄组,-1表示不在rehash,其它值表示當前rehash的進度谒麦,數(shù)據(jù)遷移是遍歷ht[0]把數(shù)據(jù)遷移到ht[1]中俄讹,rehashidx記錄了當前遍歷ht[0]的下標。
  5. iterators表示指向該dict的安全迭代器的數(shù)量绕德。
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;
2.2 dictType

dict綁定的處理函數(shù)患膛,主要有hash函數(shù),key比較函數(shù)耻蛇,key-value的拷貝和析構函數(shù)踪蹬。

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;
2.3 dictht

實際存儲key-value的結構。

typedef struct dictht {
    dictEntry **table; // 存儲數(shù)據(jù)的數(shù)組
    unsigned long size; // 數(shù)組大小
    unsigned long sizemask; // size為2^n  sizemask則為size - 1
    unsigned long used; // 哈希表中k-v的數(shù)量
} dictht;
2.4 dictEntry

字典的元存儲數(shù)據(jù)結構臣咖,k-v類型跃捣,包含存儲的鍵值對和數(shù)組中對應的鏈表的下一個元素。

typedef struct dictEntry {
    void *key; // key
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v; // val
    struct dictEntry *next; // 指向下一個元素
} dictEntry;
2.5 dictIterator

dict的迭代器夺蛇,迭代的過程可以從上面的dict結構大致可以看出疚漆。2層嵌套循環(huán),外層遍歷數(shù)組,內(nèi)層遍歷數(shù)組元素對應的鏈表娶聘。

typedef struct dictIterator {
    dict *d;  // 迭代器對應的字典
    long index; // 遍歷的hash表對應的數(shù)組下標
    int table, safe; // table對應dict->ht[2]的下標 如果是正在rehash這2個表中都會有數(shù)據(jù)
// safe=1表示該迭代器可以在迭代中間進行增刪改等操作 反之則不行
    dictEntry *entry, *nextEntry; // entry表示當前迭代器指向的元素 nextEntry表示
    /* unsafe iterator fingerprint for misuse detection. */
    long long fingerprint; // 對于unsafe的迭代器 遍歷前根據(jù)dict的值計算一個fingerprint 
    // 當?shù)瓿珊?比較當前的fingerprint和迭代前的值是否相同
    // 不同則說明在迭代過程中有增刪操作 迭代器使用不合法
} dictIterator;

三闻镶、疑問和答案

3.1 key-value的存儲格式
  1. key為void*類型,只有起始地址丸升。類型和長度由上層業(yè)務控制铆农。可能類型有是int狡耻,long墩剖,double,sds等類型夷狰。
  2. value可以是int64, uint64, double等內(nèi)置類型岭皂,也可以是void*。需要業(yè)務去解析內(nèi)存的類型和長度孵淘。
3.2 擴容判斷算法
static unsigned int dict_force_resize_ratio = 5;
#define DICT_HT_INITIAL_SIZE     4
static int _dictExpandIfNeeded(dict *d)
{
    if (dictIsRehashing(d)) return DICT_OK; // 如果已在rehash中 不再觸發(fā)

    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); // 初始化大小為4

    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); // 擴容時數(shù)組容量翻倍
    }
    return DICT_OK;
}

1.正常情況蒲障,hash表的的數(shù)據(jù)量大于數(shù)組長度時擴容歹篓。
2.特殊情況瘫证,hash表的數(shù)據(jù)量大于數(shù)組長度的5倍時,強制擴容庄撮。

3.3 擴容的容量計算
  1. hash表的初始大小為4背捌。
  2. 每次擴容時,大小翻倍洞斯。
3.4 何時數(shù)據(jù)遷移

涉及的相關代碼較多毡庆,這里不貼源碼了,數(shù)據(jù)遷移調用函數(shù)_dictRehashStep烙如。通過在源碼中搜索該函數(shù)的調用可以發(fā)現(xiàn)么抗,在對dict進行增刪查改時,會觸發(fā)該函數(shù)亚铁。

3.5 如何數(shù)據(jù)遷移

數(shù)據(jù)遷移比較簡單蝇刀,每次最多找10個空的鏈表或者處理一個有數(shù)據(jù)的鏈表。

static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        while(d->ht[0].table[d->rehashidx] == NULL) { // 遞增rehashidx找非空的鏈表 但是最多找  n*10個 找不到則返回
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        while(de) { // 找到數(shù)組中非空的鏈表后 遍歷鏈表將所有數(shù)據(jù)都進行遷移
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    // 如果已經(jīng)遷移完 設置標志位并且啟用dict->ht[0] 棄用dict->ht[1]
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}
3.6 dict的釋放

釋放的實現(xiàn)也比較簡單徘溢,不看源碼也可以猜到無非是遍歷整個dict釋放所有元素吞琐,最后釋放dict自身。相關函數(shù)dictRelease然爆,有興趣可以看源碼站粟。

3.7 dict落地的序列化

這個在序列化模塊找答案,查看dict.h/c中的所有代碼后曾雕,未找到序列化相關的函數(shù)奴烙。

3.8 rehash過程中再次觸發(fā)擴容
  1. 有可能rehash過程中再次觸發(fā)擴容。
  2. 上面源碼中可以看到,觸發(fā)二次擴容會直接返回缸沃。
3.9 迭代器遍歷實現(xiàn)
  1. 上面已經(jīng)提到了恰起,迭代器分為安全和非安全2種。區(qū)別在于迭代過程中是否能進行增刪改趾牧。
  2. 非安全迭代會在開始前根據(jù)dict的數(shù)據(jù)生成一個fingerprint检盼,在結束后再次生成fingerprint,比較2個值是否相同即可判斷中間是否有增刪改翘单。
  3. rehash過程中吨枉,2個哈希表都需要遍歷。
dictEntry *dictNext(dictIterator *iter)
{
    while (1) { // 只有遍歷完或者找到下一個非空的元素才退出循環(huán)
        if (iter->entry == NULL) {
            dictht *ht = &iter->d->ht[iter->table];
            if (iter->index == -1 && iter->table == 0) { // 迭代開始的特殊處理
                if (iter->safe)
                    iter->d->iterators++; // 表示dict當前有效的迭代器+1
                else
                    iter->fingerprint = dictFingerprint(iter->d); // 非安全模式的迭代器
            }
            iter->index++;
            if (iter->index >= (long) ht->size) { // 如果當前的hash表已遍歷完
                if (dictIsRehashing(iter->d) && iter->table == 0) { // 判斷是否在rehash中 并且當前是ht[0] 是則遍歷ht[1]
                    iter->table++;
                    iter->index = 0;
                    ht = &iter->d->ht[1];
                } else {
                    break; // 遍歷完成
                }
            }
            iter->entry = ht->table[iter->index];
        } else {
            iter->entry = iter->nextEntry;
        }
        if (iter->entry) { // 找到非空的元素
            /* We need to save the 'next' here, the iterator user
             * may delete the entry we are returning. */
            // 上面注釋解釋了為什么要保存nextEntry的原因 當entry被刪除時 nextEntry可以記錄遍歷的進度
            iter->nextEntry = iter->entry->next; 
            return iter->entry;
        }
    }
    return NULL;
}
3.10 rehash中的迭代器遍歷

從上面源碼可知哄芜,rehash過程的遍歷和普通的遍歷的差別僅在于遍歷完dict->ht[0]后需要遍歷dict->ht[1]貌亭。

3.11 迭代過程中元素的增刪

通過上面的源碼可以對安全模式下迭代器遍歷時有字典有數(shù)據(jù)的增刪的情況討論。

  1. 增加且增加到nextEntry之前认臊,新增的數(shù)據(jù)無法被遍歷到圃庭。
  2. 增加且增加到nextEntry之后,新增的數(shù)據(jù)在后續(xù)過程中會被遍歷到失晴。
  3. 刪除且刪除nextEntry之前的數(shù)據(jù)包括entry剧腻,不會對遍歷產(chǎn)生影響。
  4. 刪除nextEntry涂屁。會導致野指針的訪問书在,后續(xù)迭代訪問nextEntry時訪問的是野指針。
  5. 刪除nextEntry之后的數(shù)據(jù)拆又。不會對遍歷產(chǎn)生影響儒旬。

總結

  1. 漸進式rehash是個非常常見的情況,go中的map使用的也是漸進式rehash帖族。對于做服務器的而言栈源,應該把這個作為常識,一個操作的復雜度超過一定閾值時竖般,必須考慮分多次操作甚垦,不然有可能會引起服務器的卡頓。
  2. 非安全迭代器的校驗捻激。非安全迭代器在迭代前后會各生成一個fingerprint制轰,前后對比就能判斷中間是否有數(shù)據(jù)修改。這個想法很好胞谭,但是實現(xiàn)的方式有待商榷垃杖,源碼如下:
long long dictFingerprint(dict *d) {
    long long integers[6], hash = 0;
    int j;

    integers[0] = (long) d->ht[0].table;
    integers[1] = d->ht[0].size;
    integers[2] = d->ht[0].used;
    integers[3] = (long) d->ht[1].table;
    integers[4] = d->ht[1].size;
    integers[5] = d->ht[1].used;

    for (j = 0; j < 6; j++) {
        hash += integers[j];
        /* For the hashing step we use Tomas Wang's 64 bit integer hash. */
        hash = (~hash) + (hash << 21); // hash = (hash << 21) - hash - 1;
        hash = hash ^ (hash >> 24);
        hash = (hash + (hash << 3)) + (hash << 8); // hash * 265
        hash = hash ^ (hash >> 14);
        hash = (hash + (hash << 2)) + (hash << 4); // hash * 21
        hash = hash ^ (hash >> 28);
        hash = hash + (hash << 31);
    }
    return hash;
}

從上面的源碼可以看到,無論它使用的hash算法多好丈屹,只要size和used前后一致调俘,算出來的結果就是一樣的伶棒,也就意味著只要增加和刪除的操作是成對的,不管操作的是否是同一個元素彩库,最后dict被修改了也無法通過fingerprint看出來肤无。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市骇钦,隨后出現(xiàn)的幾起案子宛渐,更是在濱河造成了極大的恐慌,老刑警劉巖眯搭,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窥翩,死亡現(xiàn)場離奇詭異,居然都是意外死亡鳞仙,警方通過查閱死者的電腦和手機寇蚊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來棍好,“玉大人仗岸,你說我怎么就攤上這事〗梵希” “怎么了扒怖?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長提澎。 經(jīng)常有香客問我姚垃,道長念链,這世上最難降的妖魔是什么盼忌? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮掂墓,結果婚禮上谦纱,老公的妹妹穿的比我還像新娘。我一直安慰自己君编,他們只是感情好跨嘉,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吃嘿,像睡著了一般祠乃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上兑燥,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天亮瓷,我揣著相機與錄音,去河邊找鬼降瞳。 笑死嘱支,一個胖子當著我的面吹牛蚓胸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播除师,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼沛膳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了汛聚?” 一聲冷哼從身側響起锹安,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎倚舀,沒想到半個月后八毯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡瞄桨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年话速,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芯侥。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡泊交,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出柱查,到底是詐尸還是另有隱情廓俭,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布唉工,位于F島的核電站研乒,受9級特大地震影響,放射性物質發(fā)生泄漏淋硝。R本人自食惡果不足惜雹熬,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谣膳。 院中可真熱鬧竿报,春花似錦、人聲如沸继谚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽花履。三九已至芽世,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間诡壁,已是汗流浹背济瓢。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留欢峰,地道東北人葬荷。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓涨共,卻偏偏與公主長得像,于是被迫代替她去往敵國和親宠漩。 傳聞我的和親對象是個殘疾皇子举反,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348