Redis scan命令原理

scan類型命令

SCAN cursor [MATCH pattern] [COUNT count]

SSCAN KEY cursor [MATCH pattern] [COUNT count]

HSCAN  KEY cursor [MATCH pattern] [COUNT count]

ZSCAN KEY cursor [MATCH pattern] [COUNT count]

scan:迭代當前庫redis

sscan:迭代一個 set 類型數組

hscan:迭代一個hash類型,並返回相應的值服務器

zscan:迭代一個sorted set,而且返回相應的分數函數

redis是單進程單線程模型,keys和smembers這種命令可能會阻塞服務器,因此出現了scan系列的命令,經過返回一個遊標,能夠增量式迭代.性能

scan類型命令的實現

scan,sscan,hscan,zsan分別有本身的命令入口,入口中會進行參數檢測和遊標賦值,而後進入統一的入口函數:scanGenericCommand,以hscan命令爲例:spa

圖片描述
scanGenericCommand主要分四步:線程

  • 解析count和match參數.若是沒有指定count,默認返回10條數據
  • 開始迭代集合,若是是key保存爲ziplist或者intset,則一次性返回全部數據,沒有遊標(遊標值直接返回0).因爲redis設計只有數據量比較小的時候纔會保存爲ziplist或者intset,因此此處不會影響性能.

遊標在保存爲hash的時候發揮做用,具體入口函數爲dictScan,下文詳細描述。設計

  • 根據match參數過濾返回值,而且若是這個鍵已通過期也會直接過濾掉(redis中鍵過時以後並不會當即刪除)
  • 返回結果到客戶端,是一個數組,第一個值是遊標,第二個值是具體的鍵值對

dictScan中游標的實現

當迭代一個哈希表時,存在三種狀況:code

  • 從迭代開始到結束,哈希表沒有進行rehash
  • 從迭代開始到結束,哈希表進行了rehash,可是每次迭代時,哈希表要麼沒開始rehash,要麼已經結束了rehash
  • 從迭代開始到結束,某次或某幾回迭代時哈希表正在進行rehash

redis中進行rehash時會存在兩個哈希表,ht[0]與ht[1],而且是漸進式rehash(即不會一次性所有rehash);新的鍵值對會存放到ht[1]中而且會逐步將ht[0]的數據轉移到ht[1].所有rehash完畢後,ht[1]賦值給ht[0]而後清空ht[1].blog

所以遊標的實現須要兼顧以上三種狀況,以上三種狀況的遊標實現要求以下:

  • 第一種狀況比較簡單,假設redis的哈希表大小爲4,則第一次遊標爲0,讀取第一個bucket的數據,而後遊標返回1,下次讀取第二個bucket的位置,依次遍歷
  • 第二種狀況比較複雜,假設redis的哈希表大小爲4,若是rehash完後size變成了8.若是仍然按照上邊的思路返回遊標,則以下圖:

圖片描述

假設bucket0讀完以後返回了遊標1,當客戶端再次帶着遊標1返回時哈希表已經進行完rehash,而且size擴大了一倍變成了8.redis按以下方法計算一個鍵的bucket:

hash(key)&(size-1)

即若是size是4時,hash(key)&11,若是size是8時,hash(key)&111.所以當從4擴容到8時,原先在0bucket的數據會分散到0(000)與4(100)兩個bucket,bucket對應關係表以下:

圖片描述
從二進制來看,當size爲4時,hash(key)以後取低兩位即 hash(key)&11即key的bucket位置,若是size爲8時,bucket位置爲 hash(key)&111,即取低三位,當低兩位爲00時,若是第三位爲0,則爲000,若是第三位爲1,則爲100,正好是4.其餘槽位的相似.因此若是此時繼續按第一種方法遍歷,第四個bucket取到的值所有爲重複值

  • 第三種狀況,若是返回遊標1時正在進行rehash,ht[0]中的bucket 1中的部分數據可能已經rehash到 ht[1]中的bucket[1]或者bucket[5],此時必須將ht[0]和ht[1]中的相應bucket所有遍歷,不然可能會有遺漏數據

因此爲了兼顧以上三種狀況,作到不漏數據而且儘可能不重複,redis使用了一種叫作reverse binary iteration的方法.具體的遊標計算代碼以下:

圖片描述
代碼邏輯很簡單,下面示例從4變爲8和從4變爲16以及從8變爲4和從16變爲4時,這種方法爲什麼可以作到不重不漏

圖片描述
遍歷size爲4時的遊標狀態轉移爲0-2-1-3.

同理,size爲8時的遊標狀態轉移爲0-4-2-6-1-5-3-7.

size爲16時的遊標狀態轉義爲0-8-4-12-2-10-6-14-1-9-5-13-3-11-7-15

圖片描述

能夠看出,當size由小變大時,全部原來的遊標都能在大的hashTable中找到相應的位置,而且順序一致,不會重複讀取而且不會遺漏

例如size原來是4變爲了8,且第二次遍歷時rehash已經完成.此時遊標爲2,根據圖2,咱們知道size爲4時的bucket2會rehash到size爲8時的2和6.而size爲4時的bucket0rehash到size爲8時的0和4

因爲bucket 0 已經遍歷完,也即size爲8時的0,4已經遍歷,正好開始從2開始繼續遍歷,不重複也不會遺漏

繼續考慮size由大變小的狀況.假設size由16變爲了4,分兩種狀況,一種是遊標爲0,2,1,3中的一種,此時繼續讀取,也不會遺漏和重複

但若是遊標返回的不是這四種,例如返回了10,10&11以後變爲了2,因此會從2開始繼續遍歷.但因爲size爲16時的bucket2已經讀取過,而且2,10,6,14都會rehash到size爲4的bucket2,因此會形成重複讀取

size爲16時的bucket2。即有重複但不會遺漏

總結一下:redis裏邊rehash從小到大時,scan系列命令不會重複也不會遺漏.而從大到小時,有可能會形成重複但不會遺漏.

截止目前,狀況1和狀況2已經比較完美的處理了。狀況3看看如何處理

狀況3須要從ht[0]和ht[1]中都取出數據,主要的難點在於如何在size大的哈希表中找到應該取哪些bucket.redis代碼以下:

圖片描述
判斷條件爲:

v&(m0^m1)

size 4的m0爲00000011,size8的m1爲00000111,兩者異或以後取值爲00000100,即取兩者mask高位的值,而後&v,看遊標是否在高位還有值

下一個遊標的取值方法爲

v = (  ((v | m0) +1)& ~m0) | ( v & m0)

右半部分 取v的低位,左半部分取v的高位。 (v&m0)取出v的低位 例如size = 4時爲 v&00000011

左半部分 (v|m0) + 1即將v的低位都置爲1,而後+1以後會進位到v的高位,再次 & ~m0以後即取出了v的高位

總體來看每次將遊標v的高位加1.下邊舉例來看:

假設遊標返回了2,而且正在進行rehash,此時size由4變成了8 .則m0 = 00000011 v = 00000010

根據公式計算出的下一個遊標爲 ( (( 00000010|00000011) +1 ) & (11111100) )| (00000010 & 00000011) = (00000100)&(11111100)|(00000010) = (00000110) 正好是6

判斷條件爲 (00000010) & (00000011 ^ 00000111) = (00000010) & (00000100) = (00000000) 爲0,結束循環

相關文章
相關標籤/搜索