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:迭代當前庫
sscan:迭代一個 set 類型
hscan:迭代一個hash類型,并返回相應的值
zscan:迭代一個sorted set,并且返回相應的分數
redis是單進程單線程模型,keys和smembers這種命令可能會阻塞服務器,所以出現了scan系列的命令,通過返回一個游標,可以增量式迭代.
scan類型命令的實現
scan,sscan,hscan,zsan分別有自己的命令入口,入口中會進行參數檢測和游標賦值,然后進入統一的入口函數:scanGenericCommand,以hscan命令為例:
scanGenericCommand主要分四步:
- 解析count和match參數.如果沒有指定count,默認返回10條數據
- 開始迭代集合,如果是key保存為ziplist或者intset,則一次性返回所有數據,沒有游標(游標值直接返回0).由于redis設計只有數據量比較小的時候才會保存為ziplist或者intset,所以此處不會影響性能.
游標在保存為hash的時候發揮作用,具體入口函數為dictScan,下文詳細描述。
- 根據match參數過濾返回值,并且如果這個鍵已經過期也會直接過濾掉(redis中鍵過期之后并不會立即刪除)
- 返回結果到客戶端,是一個數組,第一個值是游標,第二個值是具體的鍵值對
dictScan中游標的實現
當迭代一個哈希表時,存在三種情況:
- 從迭代開始到結束,哈希表沒有進行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].
因此游標的實現需要兼顧以上三種情況,以上三種情況的游標實現要求如下:
- 第一種情況比較簡單,假設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代碼如下:
判斷條件為:
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,結束循環
總結
以上是生活随笔為你收集整理的Redis scan命令原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: lodash 核心源码学习(基于4.17
- 下一篇: 快速体验 Sentinel 集群限流功能