【Redis5源碼學習】淺析redis命令之keys篇

baiyanredis

命令語法

命令含義:查找並返回全部符合給定模式 pattern 的 key
命令格式:算法

KEYS pattern

命令實戰:數據庫

127.0.0.1:6379> keys *
1) "kkk"
2) "key1"

返回值: 根據pattern匹配後的全部鍵的集合安全

源碼分析

keys命令對應的處理函數是keysCommand():併發

void keysCommand(client *c) {
    dictIterator *di; 
    dictEntry *de;
    sds pattern = c->argv[1]->ptr; // 獲取咱們輸入的pattern
    int plen = sdslen(pattern), allkeys;
    unsigned long numkeys = 0;
    void *replylen = addDeferredMultiBulkLength(c); 

    di = dictGetSafeIterator(c->db->dict); // 初始化一個安全迭代器
    allkeys = (pattern[0] == '*' && pattern[1] == '\0'); // 判斷是否返回所有keys的集合
    while((de = dictNext(di)) != NULL) { // 遍歷整個鍵空間字典
        sds key = dictGetKey(de); // 獲取key值
        robj *keyobj;

        // 若是是返回全體鍵的集合,或者當前鍵與咱們給定的pattern匹配,那麼添加到返回列表
        if (allkeys || stringmatchlen(pattern,plen,key,sdslen(key),0)) {
            keyobj = createStringObject(key,sdslen(key));
            if (!keyIsExpired(c->db,keyobj)) { // 篩選出沒有過時的鍵
                addReplyBulk(c,keyobj); // 添加到返回列表
                numkeys++; // 返回鍵的數量++
            }
            decrRefCount(keyobj); 
        }
    }
    dictReleaseIterator(di); // 釋放安全迭代器
    setDeferredMultiBulkLength(c,replylen,numkeys); // 設置返回值的長度
}

因爲咱們使用了keys *命令,須要返回全部鍵的集合。咱們首先觀察這段代碼,它會使用一個安全迭代器,來遍歷整個鍵空間字典。在遍歷的同時,篩選出那些匹配咱們pattern以及非過時的鍵,而後返回給客戶端。因爲其遍歷的時間複雜度是和字典的大小成正比的,這樣就會致使一個問題,當鍵很是多的時候,這個鍵空間字典可能會很是大,咱們一口氣使用keys把字典從上到下遍歷一遍,會消耗很是多的時間。因爲redis是單進程的應用,長時間執行keys命令會阻塞redis進程,形成redis服務對外的不可用狀態。因此,不少公司都會禁止開發者使用keys命令,這可能致使redis服務長時間不可用。參考案例:Redis的KEYS命令引發RDS數據庫雪崩,RDS發生兩次宕機,形成幾百萬的資金損失
那麼可能你們會問了,我若是換上其餘範圍比較小的pattern去替換以前的*,不就能夠避免一次性去遍歷所有的鍵空間了嗎?可是咱們看上面的源碼,因爲它是在遍歷到每個key的時候,都會去判斷當前key是否與傳入的pattern所匹配,因此,並非咱們想象中的,只遍歷咱們傳入的pattern的鍵空間元素集合,而須要遍歷完整的鍵空間集合,在遍歷的同時篩選出符合條件的key值。其實遍歷的初始範圍並無縮小,其時間複雜度仍然爲O(N),N爲鍵空間字典的大小。函數

擴展

安全迭代器與非安全迭代器

在keys命令的遍歷過程當中,涉及到了安全迭代器的概念。與之相對的,還有非安全迭代器。那麼,迭代器是如何工做的,安全與非安全的區別有是什麼呢?咱們首先來看迭代器的存儲結構:源碼分析

typedef struct dictIterator {
    dict *d; // 指向所要遍歷的字典
    long index; // 哈希表中bucket的索引位置
    int table, safe; // table索引(參考dict結構只能爲0或1),以及迭代器是否安全的標記
    dictEntry *entry, *nextEntry; // 存儲當前entry和下一個entry
    long long fingerprint; // 指紋,只在非安全迭代的狀況下作校驗
} dictIterator;

爲了讓你們可以看明白index和table字段的做用,咱們又要貼上dict的結構了:

其中的table字段只能爲0或1。0是正常狀態下會使用的哈希表,1是rehash過程當中須要用到的過渡哈希表。而index就是每一個哈希表中01234567這個索引了。迭代器中的safe字段就是用來區分迭代器類型是安全仍是非安全的。所謂安全就是指在遍歷的過程當中,對字典的操做不會影響遍歷的結果;而非安全的迭代器可能會因爲rehash等操做,致使其遍歷結果會有所偏差,可是它的性能更好。性能

怎麼作纔會安全

在redis中,安全迭代器經過直接禁止rehash操做,來讓迭代器變得安全。那麼,爲何禁止rehash操做就安全了呢?咱們都知道,rehash操做是漸進式的。每執行一個命令,纔會作一個rehash。rehash操做會同時使用字典的兩個table。咱們考慮這樣一種狀況:假設迭代器當前正在遍歷第一個table,此時進度已經到了索引index爲3的位置,而某一個元素尚未進行rehash,咱們已經遍歷過了這個元素。那麼rehash和遍歷同時進行,假設rehash完畢,這個元素到了第二個table的index爲33的位置上。而目前迭代器的進度僅僅到了第二個table的index爲13的位置,尚未遍歷到index爲33的位置上。那麼若是繼續遍歷,因爲這個元素已經在第一個table中遍歷過一次,那麼如今會被不可避免地遍歷第二次。也就是說,因爲rehash致使同一個元素被遍歷了兩次,這就是爲何rehash會影響迭代器的遍歷結果。爲了解決以上問題,redis經過在安全迭代器運行期間禁止rehash操做,來保證迭代器是安全的。那麼究竟redis是如何判斷當前是否有安全迭代器在運行,進而來禁止rehash操做的呢?咱們首先回顧一下dict的結構:spa

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2]; // 兩個table的指針
    long rehashidx;  // rehash標誌,若是是-1則沒有在rehash
    unsigned long iterators; // 當前運行的安全迭代器的數量
} dict;

咱們看到,字典結構中的iterators字段用來描述安全迭代器的數量。若是有一個安全迭代器在運行,那麼這個字段就會++。這樣,在迭代的過程當中,字典會變得相對穩定,避免了一個元素被遍歷屢次的問題。若是當前有一個安全迭代器在運行,iterator字段必然不會爲0。當這個字段爲0的時候,才能進行rehash操做:指針

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

其實,除了安全迭代器這種簡單粗暴地禁止rehash操做以外,redis還提供了SCAN這種更高級的遍歷方式。它經過一種更爲複雜以及巧妙的算法,來保證了即便在rehash過程當中,也能保證遍歷的結果不重不漏。這就保證了rehash操做以及遍歷操做可以併發執行,同時也避免了keys在遍歷當鍵空間很大的時候超高的時間複雜度會致使redis阻塞的問題,大大提升了效率。

安全迭代器必定安全嗎

那麼繼續思考,僅僅不進行rehash操做就可以保證迭代器是安全的了嗎?因爲redis是單進程的應用,因此咱們在執行keys命令的時候,會阻塞其餘全部命令的執行。因此,在迭代器進行遍歷的時候,咱們外部是沒法經過執行命令,來對鍵空間字典進行增刪改操做的。可是redis內部的一些時間事件會有修改字典的可能性。好比:每隔一段時間掃描某個鍵是否已通過期,過時了則把它從鍵空間中刪除。這一點,我認爲即便是安全迭代器,也是沒法避免可能在遍歷期間對字典進行操做的的。好比在遍歷期間,redis某個時間事件把尚未遍歷到的元素刪除了,那麼後續迭代器再去繼續遍歷,就沒法遍歷到這個元素了。那麼如何解決這個問題呢?除非redis內部根本不在遍歷期間觸發事件並執行處理函數,不然這些操做所致使遍歷結果的細微偏差,redis是沒法避免的。

迭代器遍歷的過程

拋開上面這些細節,咱們接下來看一下具體的遍歷邏輯。首先咱們須要初始化安全迭代器:

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;
}

若是是安全迭代器,除了須要初始化以上字段以外,還須要將safe字段設置爲1:

dictIterator *dictGetSafeIterator(dict *d) {
    dictIterator *i = dictGetIterator(d); // 調用上面的方法初始化其他字段

    i->safe = 1; // 初始化safe字段
    return i;
}

回到開始的keys命令,它調用的就是dictGetSafeIterator()函數來初始化一個安全迭代器。接下來,keys命令會循環調用dictNext()方法對全部鍵空間字典中的元素作遍歷:

dictEntry *dictNext(dictIterator *iter)
{
    while (1) {
    
        // 進入這個if的兩種狀況:
        // 1. 這是迭代器第一次運行
        // 2. 當前索引鏈表中的節點已經迭代完
        if (iter->entry == NULL) {

            // 指向被遍歷的哈希表,默認爲第一個哈希表
            dictht *ht = &iter->d->ht[iter->table];

            // 僅僅第一次遍歷時執行(index初始化值爲-1)
            if (iter->index == -1 && iter->table == 0) {
            
                // 若是是安全迭代器(safe == 1),那麼更新iterators計數器
                if (iter->safe)
                    iter->d->iterators++;
                // 若是是不安全迭代器,那麼計算指紋
                else
                    iter->fingerprint = dictFingerprint(iter->d);
            }
            
            // 更新索引,繼續遍歷下一個bucket上的元素
            iter->index++;

            // 若是迭代器的當前索引大於當前被迭代的哈希表的大小
            // 那麼說明這個哈希表已經迭代完畢
            if (iter->index >= (signed) ht->size) {
                // 若是正在進行rehash操做,說明第二個哈希表也正在使用中
                // 那麼繼續對第二個哈希表進行遍歷
                if (dictIsRehashing(iter->d) && iter->table == 0) {
                    iter->table++;
                    iter->index = 0;
                    ht = &iter->d->ht[1];
                // 若是沒有rehash,則不須要遍歷第二個哈希表
                } else {
                    break;
                }
            }

            // 若是進行到這裏,說明這個哈希表並未遍歷完成
            // 更新節點指針,指向下個索引鏈表的表頭節點(index已經++過了)
            iter->entry = ht->table[iter->index];
        } else {
            // 執行到這裏,說明正在遍歷某個bucket上的鏈表(爲了解決衝突會在一個bucket後面掛接多個dictEntry,組成一個鏈表)
            iter->entry = iter->nextEntry;
        }

        // 若是當前節點不爲空,那麼記錄下該節點的下個節點的指針(即next)
        // 由於安全迭代器在運行的時候,可能會將迭代器返回的當前節點刪除,這樣就找不到next指針了
        if (iter->entry) {
            iter->nextEntry = iter->entry->next;
            return iter->entry;
        }
    }

    // 遍歷完成
    return NULL;
}

具體的遍歷過程已以註釋的形式給出了。代碼中又有一個新的概念:fingerprint指紋,下面咱們討論一下指紋的概念。

指紋的做用

在dictNext()遍歷函數中,有這樣一段代碼:

if (iter->safe) { // 若是是安全迭代器(safe == 1),那麼更新iterators計數器
     iter->d->iterators++;
} else { // 若是是不安全迭代器,那麼計算指紋
     iter->fingerprint = dictFingerprint(iter->d);
}

咱們看到,當迭代器是非安全的狀況下,它會驗證一個指紋。顧名思義,非安全的意思就是在遍歷的時候能夠進行rehash操做,這樣就會致使遍歷結果可能出現重複等問題。爲了正確地識別這種問題,redis採用了指紋機制,即在遍歷以前採集一次指紋,在遍歷完成以後再次採集指紋。若是兩次指紋比對一致,就說明遍歷結果沒有由於rehash操做的影響而改變。那麼具體如何去驗證指紋呢?驗證指紋的本質其實就是判斷字典是否由於rehash操做發生了變化:

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;
}

咱們看到,指紋驗證就是基於字典的table、size、used等字段來進行的。若是這幾個字段發生了改變,就表明rehash操做正在執行或已執行完畢。一旦有rehash操做在執行,那麼有可能就會致使遍歷結果受到影響。因此,非安全迭代器的指紋驗證可以很好地發現rehash操做對遍歷結果產生影響的可能性。

相關文章
相關標籤/搜索