Redis遍历方式思考--字典扩容方式
生活随笔
收集整理的這篇文章主要介紹了
Redis遍历方式思考--字典扩容方式
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
全量遍歷keys
- 工作中線上Redis維護,有時候我們需要查詢特定前綴的緩存key列表來手動處理數據。可能是修改值,刪除key。那么怎么才能快速的從海量的key中查找到對應的前綴匹配項。
- Redis提供了一下簡單的指令,例如keys用來滿足特定正則下的key如下:
- 以上命令特別簡單,只需要提供一個簡單的正則字符串即可,但是缺點明顯:
- 沒有offset,limit參數,一次查詢所有滿足條件的key,如果實例中有幾百萬key匹配到,那查詢到了也沒有任何意義。
- keys算法是遍歷算法,復雜度是O(n),如果Redis實例中有千萬級數據key,這個指令就會導致Redis服務器卡頓,所有讀寫Redis的其他指令都會被延后甚至會超時報錯,因為Redis是單線程,順序執行所有指令,其他指令必須等到當前keys指令執行完才可以繼續
大海撈針指令scan
- scan 指令有一下幾個特點:
- 復雜度也是O(n)。但是scan命令可以通過游標分布進行查詢,不會阻塞線程,相當于分頁查找
- 提供limit參數,可以控制每次放回結果的條數,limit只是一個hint,返回的結果可多可少。
- 同keys一樣,他提供了模式匹配功能
- 服務器不需要為游標保存狀態,右邊的唯一狀態就是scan返回給客戶端的游標整數
- 返回的結果可能會重復,需要客戶端去重
- 變量過程如果有數據修改,改動后的數據能否查詢到是不確定的
- 單次返回的結果是空的并不意味著遍歷結束,而要看返回的游標值是否為零
scan基本用法
- sacn提供三個參數,第一個cursor整數值,第二個是key的正則模式,第三個是遍歷limit hint。scan每次查詢會返回一個游標值,標識現在已經遍歷到此處游標位置,我們下一次遍歷可以按照游標開始,如下
- 查詢過程中,我們設定limit = 1000,但是并不是每次都能查詢到一千條數據,因為limit不是限制返回的數量,二手限制服務器每次遍歷的Redis服務器存儲數據的字典槽個數。如果將limit設置為10,可能返回0個數據,但是游標值不為0 ,那是因為前10個槽中沒有數據而已。
字典結構
- Redis中所有key存儲在一個很大的字典中,這個字典結構和java中HashMap類似,如下圖中所示,他是一位數組結構,加上二維鏈表結構。第一位數組的大小總數(2 ^ n) 擴容一次數組,大小空間加倍 (2^n+1)
- scan指令返回的游標就是第一位數組的位置索引,這個就是上面說的數據槽(slot),如果不考慮字典的擴容,縮容直接按數組下標逐個遍歷就行。limit參數標識需要遍歷的槽位數,之所以返回的結果可能多可能少,是因為不是所有的槽位上都掛載了鏈表,有可能槽位是空的,每次遍歷都會將limit數量的槽位上掛接的所有鏈表元素進行模式匹配過濾后一起返回客戶端。
scan遍歷順序
- scan遍歷的凡是不是從一維數組的第0 位開始遍歷,而是采用高位進位的方法來遍歷。之所以用這樣的方式是考慮到Redis的擴容規則,當擴容的時候這樣遍歷就能避免槽位的重復遍歷,和遺漏
- 如下圖,是普通的加法和高位進位加法的區別:
普通遍歷
高位進位遍歷
- 上圖中看出高位進位加法也是一樣遵循二進制的規則,只不過進位從左邊開始增加移動,同普通加法正好相反,但是最終還是可以遍歷所有槽位并且沒有重復。
字典擴容
- java中HashMap也有擴容的概念,當LoadFactor達到閥值的時候,需要重新分配一個新的2倍大小的數組,然后將所有元素全部rehash掛到新的數組下面。rehash是將原始的hash值對數組長度取模運算,因為長度變量,所有每個元素掛載的位置槽就可能變化。有因為數組長度是2的n次方,取模運算等價于位與操作(&);
- Redis中擴容方法如下圖中所示,當字典長度由8 為擴容到16位,那么3號槽的數據011 將會被rehash到3號槽和11號槽中。也就是該槽位鏈表中大約有一半的元素還在3號槽位中,其他元素被放到11號槽位中,11 這個數字正好是1011,就是3 的二進制數011 高位添加一個1.
- 按如上方式加上槽位二進制是XXX,你們該槽位中元素將被rehash到0XXX和1XXX中(XXX+8),如果字典長度由16 擴容到
32,你們XXXX中元素rehash后到0XXXX 和1XXXX(XXXX+16)
對比擴容前,縮容后的遍歷順序
- 如上擴容縮容示意圖,我們發現用高位進位加法的遍歷方式,rehash后的槽位在遍歷順序上是相鄰的,
- 擴容情況:如上加入我們要遍歷100 這個槽位,那么擴容后,當前槽位上所有元素到新的槽位0100,1100,也就是在槽位二進制高位添加0,1。這時候,我們可以直接從0100開始往后遍歷,而按照scan的變量規則,下一個正好是1100(高位加1),之前的已經都遍歷完了之后的按照這個變量方式也不會遺漏。
- 縮容情況:加入當前變量101,那么縮容后當前槽位所有的元素對應的01,也就是去掉高位的1,這個時候我們可以直接從01這個槽位繼續向后遍歷,01 之前的槽位已經遍歷完了,這樣就可以避免縮容重復遍歷,縮容有一點不一樣的地方是,會對101中的元素進行遍歷,因為縮容的時候01 中的數據是結合了001 和101 鏈表中所有的數據。
漸進式rehash
- java中HashMap在擴容時候,會一次性將舊的數據數組下掛載的元素全部轉移到新的數組下,如果Hashmap中元素特別多,線程會出現卡頓現象。Redis為了解決這個問題采用漸進式rehash
- 同事保留新舊數組。讓后在定時任務中對后續hash的指令操作漸漸的將數組中掛載的元素遷移到新的數組中區。scan此時遍歷處于rehash階段的字典需要同時訪問新舊兩個數組結構。如果在就數組下面找不到元素,需要到新數組中在找一次。
更多scan指令
- scan指令是一系列指令,除了可以遍歷所有key,還有其他指定數據結構的特定指令。例如zscan遍歷zset集合元素,hscan遍歷hash字典元素,sscan遍歷set集合元素
- 原理同scan類似,因為hash底層就是字典,set也是特殊hash(所有value都是null)zset內部也是使用字典來存儲所有元素內容
大key掃描
- 有時候因為業務使用不當,在Redis實例中存在一個很大的對象,比如一個很大的zset。這樣的對象給Redis繼續數據遷移帶來很大的問題。在數據遷移過程中集群環境下key太大,導致遷移數據變慢造成服務卡頓。同時擴容時候會一次性申請更大的一塊內存,也會導致卡頓。如果這個key被刪除內存會被一次性回收卡頓現象也會再次產生
- 平時應該避免大key的產生
- 如果Redis內存波動大,極有可能因為大key導致的,這時候需要定位具體那個key,進一步定位出具體的業務,然后在改進。
- scan指令遍歷,用type指令獲取key類型,size或者len得到大小,用腳本掃描出來,不過此方法比較繁瑣
- Redis官方在redis-cli指令中提供這樣的掃描功能,我們可以直接用如下:
- 以上命令會自動的查詢大key,但是會導致Redis的ops(operation pre seconds 每秒操作次數)大幅提高,我們可以增加一個休眠時間,如下:
- 上面指令每隔100條scan指令休眠0.1秒,這樣ops就不會劇烈提高,只是掃描時間變長而已。
總結
以上是生活随笔為你收集整理的Redis遍历方式思考--字典扩容方式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构与算法--经典10大排序算法(动
- 下一篇: Redis高效性探索--线程IO模型,通