跋山涉水 —— 深刻 Redis 字典遍歷

Redis 字典的遍歷過程邏輯比較複雜,互聯網上對這一塊的分析講解很是少。我也花了很多時間對源碼的細節進行了整理,將我我的對字典遍歷邏輯的理解呈現給各位讀者。也許讀者們對字典的遍歷過程有比我更好的理解,還請不吝指教。數組

一邊遍歷一邊修改

咱們知道 Redis 對象樹的主幹是一個字典,若是對象不少,這個主幹字典也會很大。當咱們使用 keys 命令搜尋指定模式的 key 時,它會遍歷整個主幹字典。值得注意的是,在遍歷的過程當中,若是知足模式匹配條件的 key 被找到了,還須要判斷 key 指向的對象是否已通過期。若是過時了就須要從主幹字典中將該 key 刪除。安全

void keysCommand(client *c) {
    dictIterator *di; // 迭代器
    dictEntry *de; // 迭代器當前的entry
    sds pattern = c->argv[1]->ptr; // keys的匹配模式參數
    int plen = sdslen(pattern);
    int allkeys; // 是否要獲取全部key,用於keys *這樣的指令
    unsigned long numkeys = 0;
    void *replylen = addDeferredMultiBulkLength(c);

    // why safe? 
    di = dictGetSafeIterator(c->db->dict);
    allkeys = (pattern[0] == '*' && pattern[1] == '\0');
    while((de = dictNext(di)) != NULL) {
        sds key = dictGetKey(de);
        robj *keyobj;

        if (allkeys || stringmatchlen(pattern,plen,key,sdslen(key),0)) {
            keyobj = createStringObject(key,sdslen(key));
            // 判斷是否過時,過時了要刪除元素
            if (expireIfNeeded(c->db,keyobj) == 0) {
                addReplyBulk(c,keyobj);
                numkeys++;
            }
            decrRefCount(keyobj);
        }
    }
    dictReleaseIterator(di);
    setDeferredMultiBulkLength(c,replylen,numkeys);
}
複製代碼

那麼,你是否想到了其中的困難之處,在遍歷字典的時候還須要修改字典,會不會出現指針安全問題?bash

重複遍歷

字典在擴容的時候要進行漸進式遷移,會存在新舊兩個 hashtable。遍歷須要對這兩個 hashtable 依次進行,先遍歷完舊的 hashtable,再繼續遍歷新的 hashtable。若是在遍歷的過程當中進行了 rehashStep,將已經遍歷過的舊的 hashtable 的元素遷移到了新的 hashtable中,那麼遍歷會不會出現元素的重複?這也是遍歷須要考慮的疑難之處,下面咱們來看看 Redis 是如何解決這個問題的。服務器

迭代器的結構

Redis 爲字典的遍歷提供了 2 種迭代器,一種是安全迭代器,另外一種是不安全迭代器。微信

typedef struct dictIterator {
    dict *d; // 目標字典對象
    long index; // 當前遍歷的槽位置,初始化爲-1
    int table; // ht[0] or ht[1]
    int safe; // 這個屬性很是關鍵,它表示迭代器是否安全
    dictEntry *entry; // 迭代器當前指向的對象
    dictEntry *nextEntry; // 迭代器下一個指向的對象
    long long fingerprint; // 迭代器指紋,放置迭代過程當中字典被修改
} dictIterator;

// 獲取非安全迭代器,只讀迭代器,容許rehashStep
dictIterator *dictGetIterator(dict *d) {
    dictIterator *iter = zmalloc(sizeof(*iter));

    iter->d = d;
    iter->table = 0;
    iter->index = -1;
    iter->safe = 0;
    iter->entry = NULL;
    iter->nextEntry = NULL;
    return iter;
}

// 獲取安全迭代器,容許觸發過時處理,禁止rehashStep
dictIterator *dictGetSafeIterator(dict *d) {
    dictIterator *i = dictGetIterator(d);

    i->safe = 1;
    return i;
}
複製代碼

迭代器的「安全」指的是在遍歷過程當中能夠對字典進行查找和修改,不用感到擔憂,由於查找和修改會觸發過時判斷,會刪除內部元素。「安全」的另外一層意思是迭代過程當中不會出現元素重複,爲了保證不重複,就會禁止 rehashStep。函數

而「不安全」的迭代器是指遍歷過程當中字典是隻讀的,你不能夠修改,你只能調用 dictNext 對字典進行持續遍歷,不得調用任何可能觸發過時判斷的函數。不過好處是不影響 rehash,代價就是遍歷的元素可能會出現重複。ui

安全迭代器在剛開始遍歷時,會給字典打上一個標記,有了這個標記,rehashStep 就不會執行,遍歷時元素就不會出現重複。spa

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx;
    // 這個就是標記,它表示當前加在字典上的安全迭代器的數量
    unsigned long iterators;
} dict;

// 若是存在安全的迭代器,就禁止rehash
static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}
複製代碼

迭代過程

安全的迭代器在遍歷過程當中容許刪除元素,意味着字典第一維數組下面掛接的鏈表中的元素可能會被摘走,元素的 next 指針就會發生變更,這是否會影響迭代過程呢?下面咱們仔細研究一下迭代函數的代碼邏輯。指針

dictEntry *dictNext(dictIterator *iter)
{
    while (1) {
        if (iter->entry == NULL) {
            // 遍歷一個新槽位下面的鏈表,數組的index往前移動了
            dictht *ht = &iter->d->ht[iter->table];
            if (iter->index == -1 && iter->table == 0) {
                // 第一次遍歷,剛剛進入遍歷過程
                // 也就是ht[0]數組的第一個元素下面的鏈表
                if (iter->safe) {
                  // 給字典打安全標記,禁止字典進行rehash
                  iter->d->iterators++;
                } else {
                  // 記錄迭代器指紋,就比如字典的md5值
                  // 若是遍歷過程當中字典有任何變更,指紋就會改變
                  iter->fingerprint = dictFingerprint(iter->d);
                }      
            }
            iter->index++; // index=0,正式進入第一個槽位
            if (iter->index >= (long) ht->size) {
                // 最後一個槽位都遍歷完了
                if (dictIsRehashing(iter->d) && iter->table == 0) {
                    // 若是處於rehash中,那就繼續遍歷第二個 hashtable
                    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) {
            // 將下一個元素也記錄到迭代器中,這點很是關鍵
            // 防止安全迭代過程當中當前元素被過時刪除後,找不到下一個須要遍歷的元素
            
            // 試想若是後面發生了rehash,當前遍歷的鏈表被打散了,會發生什麼
            // 這裏要使勁發揮本身的想象力來理解
            // 舊的鏈表將一分爲二,打散後從新掛接到新數組的兩個槽位下
            // 結果就是會致使當前鏈表上的元素會重複遍歷
            
            // 若是rehash的鏈表是index前面的鏈表,那麼這部分鏈表也會被重複遍歷
            iter->nextEntry = iter->entry->next;
            return iter->entry;
        }
    }
    return NULL;
}

// 遍歷完成後要釋放迭代器,安全迭代器須要去掉字典的禁止rehash的標記
// 非安全迭代器還須要檢查指紋,若是有變更,服務器就會奔潰(failfast)
void dictReleaseIterator(dictIterator *iter)
{
    if (!(iter->index == -1 && iter->table == 0)) {
        if (iter->safe)
            iter->d->iterators--; // 去掉禁止rehash的標記
        else
            assert(iter->fingerprint == dictFingerprint(iter->d));
    }
    zfree(iter);
}

// 計算字典的指紋,就是將字典的關鍵字段進行按位糅合到一塊兒
// 這樣只要有任意的結構變更,指紋都會發生變化
// 若是隻是某個元素的value被修改了,指紋不會發生變更
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];
        hash = (~hash) + (hash << 21);
        hash = hash ^ (hash >> 24);
        hash = (hash + (hash << 3)) + (hash << 8);
        hash = hash ^ (hash >> 14);
        hash = (hash + (hash << 2)) + (hash << 4);
        hash = hash ^ (hash >> 28);
        hash = hash + (hash << 31);
    }
    return hash;
}
複製代碼

值得注意的是在字典擴容時進行rehash,將舊數組中的鏈表遷移到新的數組中。某個具體槽位下的鏈表只可能會遷移到新數組的兩個槽位中。code

hash mod 2^n = k
hash mod 2^(n+1) = k or k+2^n
複製代碼

迭代器的選擇

除了keys指令使用了安全迭代器,由於結果不容許重複。那還有其它的地方使用了安全迭代器麼,什麼狀況下遍歷適合使用非安全迭代器呢?

簡單一點說,那就是若是遍歷過程當中不容許出現重複,那就使用SafeIterator,好比下面的兩種狀況

  1. bgaofrewrite須要遍歷全部對象轉換稱操做指令進行持久化,絕對不容許出現重複
  2. bgsave也須要遍歷全部對象來持久化,一樣不容許出現重複

若是遍歷過程當中須要處理元素過時,須要對字典進行修改,那也必須使用SafeIterator,由於非安全的迭代器是隻讀的。

其它狀況下,也就是容許遍歷過程當中出現個別元素重複,不須要對字典進行結構性修改的狀況下一概使用非安全迭代器。

思考

請繼續思考rehash對非安全遍歷過程的影響,會重複哪些元素,重複的元素會很是多麼仍是隻是少許重複?

微信掃一掃關注公衆號「碼洞」,一步兩步帶你「碼出個將來」。

相關文章
相關標籤/搜索