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,并且返回相應的分數(shù)
redis是單進程單線程模型,keys和smembers這種命令可能會阻塞服務器,所以出現(xiàn)了scan系列的命令,通過返回一個游標,可以增量式迭代.
scan類型命令的實現(xiàn)
scan,sscan,hscan,zsan分別有自己的命令入口,入口中會進行參數(shù)檢測和游標賦值,然后進入統(tǒng)一的入口函數(shù):scanGenericCommand,以hscan命令為例:
scanGenericCommand主要分四步:
- 解析count和match參數(shù).如果沒有指定count,默認返回10條數(shù)據(jù)
- 開始迭代集合,如果是key保存為ziplist或者intset,則一次性返回所有數(shù)據(jù),沒有游標(游標值直接返回0).由于redis設計只有數(shù)據(jù)量比較小的時候才會保存為ziplist或者intset,所以此處不會影響性能.
游標在保存為hash的時候發(fā)揮作用,具體入口函數(shù)為dictScan,下文詳細描述。
- 根據(jù)match參數(shù)過濾返回值,并且如果這個鍵已經(jīng)過期也會直接過濾掉(redis中鍵過期之后并不會立即刪除)
- 返回結(jié)果到客戶端,是一個數(shù)組,第一個值是游標,第二個值是具體的鍵值對
dictScan中游標的實現(xiàn)
當?shù)粋€哈希表時,存在三種情況:
- 從迭代開始到結(jié)束,哈希表沒有進行rehash
- 從迭代開始到結(jié)束,哈希表進行了rehash,但是每次迭代時,哈希表要么沒開始rehash,要么已經(jīng)結(jié)束了rehash
- 從迭代開始到結(jié)束,某次或某幾次迭代時哈希表正在進行rehash
redis中進行rehash時會存在兩個哈希表,ht[0]與ht[1],并且是漸進式rehash(即不會一次性全部rehash);新的鍵值對會存放到ht[1]中并且會逐步將ht[0]的數(shù)據(jù)轉(zhuǎn)移到ht[1].全部rehash完畢后,ht[1]賦值給ht[0]然后清空ht[1].
因此游標的實現(xiàn)需要兼顧以上三種情況,以上三種情況的游標實現(xiàn)要求如下:
- 第一種情況比較簡單,假設redis的哈希表大小為4,則第一次游標為0,讀取第一個bucket的數(shù)據(jù),然后游標返回1,下次讀取第二個bucket的位置,依次遍歷
- 第二種情況比較復雜,假設redis的哈希表大小為4,如果rehash完后size變成了8.如果仍然按照上邊的思路返回游標,則如下圖:
假設bucket0讀完之后返回了游標1,當客戶端再次帶著游標1返回時哈希表已經(jīng)進行完rehash,并且size擴大了一倍變成了8.redis按如下方法計算一個鍵的bucket:
hash(key)&(size-1)即如果size是4時,hash(key)&11,如果size是8時,hash(key)&111.因此當從4擴容到8時,原先在0bucket的數(shù)據(jù)會分散到0(000)與4(100)兩個bucket,bucket對應關(guān)系表如下:
從二進制來看,當size為4時,hash(key)之后取低兩位即 hash(key)&11即key的bucket位置,如果size為8時,bucket位置為 hash(key)&111,即取低三位,當?shù)蛢晌粸?0時,如果第三位為0,則為000,如果第三位為1,則為100,正好是4.其他槽位的類似.所以如果此時繼續(xù)按第一種方法遍歷,第四個bucket取到的值全部為重復值
- 第三種情況,如果返回游標1時正在進行rehash,ht[0]中的bucket 1中的部分數(shù)據(jù)可能已經(jīng)rehash到 ht[1]中的bucket[1]或者bucket[5],此時必須將ht[0]和ht[1]中的相應bucket全部遍歷,否則可能會有遺漏數(shù)據(jù)
所以為了兼顧以上三種情況,做到不漏數(shù)據(jù)并且盡量不重復,redis使用了一種叫做reverse binary iteration的方法.具體的游標計算代碼如下:
代碼邏輯很簡單,下面示例從4變?yōu)?和從4變?yōu)?6以及從8變?yōu)?和從16變?yōu)?時,這種方法為何能夠做到不重不漏
遍歷size為4時的游標狀態(tài)轉(zhuǎn)移為0-2-1-3.
同理,size為8時的游標狀態(tài)轉(zhuǎn)移為0-4-2-6-1-5-3-7.
size為16時的游標狀態(tài)轉(zhuǎn)義為0-8-4-12-2-10-6-14-1-9-5-13-3-11-7-15
可以看出,當size由小變大時,所有原來的游標都能在大的hashTable中找到相應的位置,并且順序一致,不會重復讀取并且不會遺漏
例如size原來是4變?yōu)榱?,且第二次遍歷時rehash已經(jīng)完成.此時游標為2,根據(jù)圖2,我們知道size為4時的bucket2會rehash到size為8時的2和6.而size為4時的bucket0rehash到size為8時的0和4
由于bucket 0 已經(jīng)遍歷完,也即size為8時的0,4已經(jīng)遍歷,正好開始從2開始繼續(xù)遍歷,不重復也不會遺漏
繼續(xù)考慮size由大變小的情況.假設size由16變?yōu)榱?,分兩種情況,一種是游標為0,2,1,3中的一種,此時繼續(xù)讀取,也不會遺漏和重復
但如果游標返回的不是這四種,例如返回了10,10&11之后變?yōu)榱?,所以會從2開始繼續(xù)遍歷.但由于size為16時的bucket2已經(jīng)讀取過,并且2,10,6,14都會rehash到size為4的bucket2,所以會造成重復讀取
size為16時的bucket2。即有重復但不會遺漏
總結(jié)一下:redis里邊rehash從小到大時,scan系列命令不會重復也不會遺漏.而從大到小時,有可能會造成重復但不會遺漏.
截止目前,情況1和情況2已經(jīng)比較完美的處理了。情況3看看如何處理
情況3需要從ht[0]和ht[1]中都取出數(shù)據(jù),主要的難點在于如何在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
根據(jù)公式計算出的下一個游標為 ( (( 00000010|00000011) +1 ) & (11111100) )| (00000010 & 00000011) = (00000100)&(11111100)|(00000010) = (00000110) 正好是6
判斷條件為 (00000010) & (00000011 ^ 00000111) = (00000010) & (00000100) = (00000000) 為0,結(jié)束循環(huán)
總結(jié)
以上是生活随笔為你收集整理的Redis scan命令原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: lodash 核心源码学习(基于4.17
- 下一篇: 邀您共赴数据库学术顶会ICDE 2019