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操做對遍歷結果產生影響的可能性。