1、前言
Redis以高性能著稱,但性能再好,在面對海量數據時,若不正確的使用,也終將會有性能瓶頸,甚至造成服務宕機。
在實際項目中你是否會有以下疑問?
- 如何訪問Redis中的海量數據,卻不影響其他請求訪問Redis?
- Redis中有百萬/千萬數據,如何高效訪問?
- Redis中數據量太大,如何既保證快速訪問,又不至於使服務宕機?
以上問題亦是Redis面試的高頻問題。
《玩轉Redis》系列文章主要講述Redis的基礎及中高級應用,文章基於Redis5.0.4+,歡迎前往CSDN、訂閱號、開源中國、掘金等平臺搜索【zxiaofan】查看系列文章。
2、思考
Q1:爲什麼Redis中的數據量很大時,某些數據操作會導致Redis卡頓,甚至宕機?
A1:Redis是單線程服務,所有指令都是順序執行,當某一指令耗時很長時,就會阻塞後續的指令執行。當被積壓的指令越來越多時,Redis服務佔用CPU將不斷升高,最終導致Redis實例崩潰甚至服務器宕機。
Q2:利用萬能的keys命令查詢任何想查的數據?
A2:自己電腦幾萬條數據玩玩就好了,線上使用keys命令,Excuse me?你想捲鋪蓋走人了吧。
++“某公司php工程師執行redis keys * 導致數據庫宕機!
技術部發生2起本年度PO級特大事故,造成公司資金損失400萬。”++ 這條新聞記憶猶新,警鐘長鳴!
Q3:Redis中海量數據的正確操作方式
A3:利用SCAN系列命令(SCAN、SSCAN、HSCAN、ZSCAN)完成數據迭代。
Redis的【SCAN系列命令】你瞭解多少呢?
3、SCAN系列命令詳解
SCAN系列命令,並不單純指代SCAN命令,還包含SSCAN、HSCAN、ZSCAN,每種命令操作對象是有區別的,但用法及功能基本相同。
3.1、SCAN系列命令對比分析
- cursor:迭代遊標;
- MATCH:數據匹配模式;
- COUNT:迭代返回數量;
命令 | 功能 | 參數 | 返回值 |
---|---|---|---|
SCAN | 基於遊標迭代DB | cursor [MATCH pattern] [COUNT count] | 返回數組,第一個值是下一次迭代的遊標(無符號64bit),第2個值是元素列表(key列表) |
SSCAN | 基於遊標迭代Sets | key cursor [MATCH pattern] [COUNT count] | 返回數組,第一個值是下一次迭代的遊標(無符號64bit),第2個值是元素列表 |
HSCAN | 基於遊標迭代Hashes | key cursor [MATCH pattern] [COUNT count] | 返回數組,第2個值是field-value列表 |
ZSCAN | 基於遊標迭代ZSets | key cursor [MATCH pattern] [COUNT count] | 返回數組,第2個值是member-score列表 |
3.2、SCAN系列命令注意事項
- SCAN的參數沒有key,因爲其迭代對象是DB內數據;
- 返回值都是數組,第一個值都是下一次迭代遊標;
- 時間複雜度:每次請求都是O(1),完成所有迭代需要O(N),N是元素數量;
- 可用版本:version >= 2.8.0;
3.3、SCAN系列命令詳解
3.3.1、 增量迭代,可用於生產環境
- 並不像KEYS、SMEMBERS一樣是全量迭代,對大集合執行時可能阻塞服務很長時間;
3.3.2、不保證準確結果
- SMEMBERS可以返回整個set的元素,而SCAN這類增量迭代命令可能出現迭代過程中元素被改變,所以並不能保證準確的返回結果;
3.3.3、基於遊標迭代
- SCAN基於遊標迭代,每次請求將返回下一次需要使用的遊標;
- 遊標cursor可以比DB元素總量大,可以爲負數;
- 錯誤遊標:使用間斷(不是迭代返回的)、負數、超出範圍或其他非法遊標,迭代不會報錯,可能產生未定義行爲(無法保證準確性);
3.3.4、迭代結束標記
- SCAN返回的遊標不一定遞增,某次迭代返回的元素數量可能爲0;
- 返回元素列表爲空,不代表迭代結束;
- 一個完整的迭代:SCAN遊標從0開始,返回遊標爲0結束;
- 迭代狀態由返回的遊標控制。可以併發執行迭代;可隨時終止迭代;
3.3.5、迭代完整性
- 遍歷開始到遍歷結束一直存在的數據,一定能被迭代返回;
- 同一個元素可能返回多次,數據去重應由應用程序完成;
- 在迭代過程中增刪的元素,可能返回,可能不返回;
- 當數據類型是sets(由integer組成)、hashes、sorted sets且集合較小時,迭代將返回整個集合的數據,與count無關;
- 迭代結束保證:元素添加速率小於迭代速率。
3.3.6、why有時迭代直接返回整個集合
- 底層數據結構是hash時,如果數據量較小,Redis有內存優化策略,會使用緊湊的壓縮編碼。此時SCAN操作並不是返回有意義的遊標,而是迭代整個集合;
- 數據量較小?參見官方memory-optimization(內存優化)說明。
3.3.7、參數count說明
- count默認值是10;
- 數據集較大時,如果沒有使用match,返回元素爲count或比count略大;
- 每次迭代的count參數值可以不同,只要使用上次迭代返回的遊標即可;
3.3.8、參數match說明
- 和keys的pattern類似;
- MATCH操作是在檢索出數據到返回元素前的期間執行,所以如果被匹配的元素較少,那麼可能多次迭代返回的元素列表均爲空;
4、SCAN系列命令示例
4.1、SCAN示例
詳見《5.2、部分問題解答》
4.2、SSCAN示例
// SSCAN示例 @zxiaofan
127.0.0.1:6378> SADD sscantest sscantest:1 1 sscantest:2 2 sscantest:3 3 sscantest:4 4 sscantest:1a 1a sscantest:2a 2a sscantest:1ab 1ab sscantest:a1 a1 sscantest:aa1 aa1
(integer) 0
// MATCH ?:無匹配數據
127.0.0.1:6378> SSCAN sscantest 0 MATCH ? COUNT 1
1) "24"
2) (empty list or set)
127.0.0.1:6378> SSCAN sscantest 24 MATCH ? COUNT 1
1) "20"
2) (empty list or set)
127.0.0.1:6378> SSCAN sscantest 0 MATCH * COUNT 1
1) "24"
2) 1) "sscantest:3"
2) "sscantest:2a"
127.0.0.1:6378> SSCAN sscantest 24 MATCH * COUNT 1
1) "20"
2) 1) "a1"
4.3、HSCAN示例
// HSCAN示例 @zxiaofan
127.0.0.1:6378> HMSET hscantest hscantest:1 1 hscantest:2 2 hscantest:3 3 hscantest:4 4 hscantest:1a 1a hscantest:2a 2a hscantest:1ab 1ab hscantest:a1 a1 hscantest:aa1 aa1
OK
127.0.0.1:6378> HSCAN hscantest 0 MATCH hscantest*a COUNT 20
1) "0"
2) 1) "hscantest:1a"
2) "1a"
3) "hscantest:2a"
4) "2a"
127.0.0.1:6378> HSCAN hscantest 0 MATCH hscantest*a COUNT 2
1) "0"
2) 1) "hscantest:1a"
2) "1a"
3) "hscantest:2a"
4) "2a"
127.0.0.1:6378>
從HSCAN示例可以看出,即使count參數爲2,也返回了所有匹配的結果。這就是先前提到的,數據量較小時,直接返回所有數據。
4.4、ZSCAN示例
// ZSCAN示例 @zxiaofan
// 【移除】並彈出count個分數最大的元素,count默認爲1
127.0.0.1:6378> ZPOPMAX zscantest 20
1) "sscantest:1ab"
2) "6"
3) "sscantest:2a"
4) "5"
5) "sscantest:1a"
6) "4"
7) "sscantest:3"
8) "3"
9) "zscantest:1"
10) "2"
11) "sscantest:2"
12) "2"
13) "test1"
14) "1"
15) "sscantest:1"
16) "1"
127.0.0.1:6378> ZPOPMAX zscantest 20
(empty list or set)
127.0.0.1:6378> ZADD zscantest 1 zscantest:1 2 zscantest:2 3 zscantest:3 4 zscantest:1a 5 zscantest:2a 6 zscantest:1ab 7 zscantest:a1 8 zscantest:aa1
(integer) 8
// NX:不存在才添加;CH:返回被改變(含新增)的元素個數
127.0.0.1:6378> ZADD zscantest NX CH 1 test1 2 zscantest:1
(integer) 1
127.0.0.1:6378> ZSCAN zscantest 0 MATCH *a COUNT 5
1) "0"
2) 1) "zscantest:1a"
2) "4"
3) "zscantest:2a"
4) "5"
127.0.0.1:6378>
5、總結
5.1、看看面試時你能答上幾個問題
- SCAN迭代可以併發嗎?
- SCAN返回數據爲空就是迭代結束了嗎?
- 如果首次迭代cursor參數不是0,能實現完整迭代嗎?
- 可以嚴格控制每次迭代返回的數據量嗎?
- 迭代返回的數據一定完整嗎?
- 爲什麼迭代返回的元素列表可能爲空?
5.2、部分問題解答
5.2.1、SCAN返回數據爲空就是迭代結束了嗎
// SCAN返回數據爲空就是迭代結束了嗎? @zxiaofan
127.0.0.1:6378> keys k?
1) "k1"
2) "k2"
127.0.0.1:6378> SCAN 0 MATCH k?
1) "88"
2) (empty list or set)
127.0.0.1:6378> SCAN 88 MATCH k?
1) "34"
2) 1) "k1"
127.0.0.1:6378> SCAN 34 MATCH k?
1) "122"
2) (empty list or set)
127.0.0.1:6378> SCAN 122 MATCH k?
1) "14"
2) (empty list or set)
127.0.0.1:6378> SCAN 14 MATCH k?
1) "33"
2) (empty list or set)
127.0.0.1:6378> SCAN 33 MATCH k?
1) "53"
2) (empty list or set)
127.0.0.1:6378> SCAN 53 MATCH k?
1) "93"
2) (empty list or set)
127.0.0.1:6378> SCAN 93 MATCH k?
1) "107"
2) 1) "k2"
127.0.0.1:6378> SCAN 107 MATCH k?
1) "79"
2) (empty list or set)
127.0.0.1:6378> SCAN 79 MATCH k?
1) "0"
2) (empty list or set)
127.0.0.1:6378>
看上述示例,匹配“k?”的數據實際有2條“k1”、“k2”,在整個迭代過程中,多次返回數據爲空,但是迭代未曾結束(因爲“k1”、“k2”沒有全部迭代返回)。
所以,只有當遊標返回爲0時,才能說明迭代結束了。
5.2.2、如果首次迭代cursor參數不是0,能實現完整迭代嗎?
// 如果首次迭代cursor參數不是0,能實現完整迭代嗎? @zxiaofan
127.0.0.1:6378> keys k?
1) "k1"
2) "k2"
127.0.0.1:6378> SCAN 66 MATCH k?
1) "122"
2) (empty list or set)
127.0.0.1:6378> SCAN 122 MATCH k?
1) "14"
2) (empty list or set)
127.0.0.1:6378> SCAN 14 MATCH k?
1) "33"
2) (empty list or set)
127.0.0.1:6378> SCAN 33 MATCH k?
1) "53"
2) (empty list or set)
127.0.0.1:6378> SCAN 53 MATCH k?
1) "93"
2) (empty list or set)
127.0.0.1:6378> SCAN 93 MATCH k?
1) "107"
2) 1) "k2"
127.0.0.1:6378> SCAN 107 MATCH k?
1) "79"
2) (empty list or set)
127.0.0.1:6378> SCAN 79 MATCH k?
1) "0"
2) (empty list or set)
127.0.0.1:6378>
看上述示例,匹配“k?”的數據實際有2條“k1”、“k2”,當第一次SCAN使用cursor爲66,我們可以發現經過多次迭代,遊標返回爲0時,“k1”一直未曾被迭代返回。
所以,如果首次迭代cursor參數不是0,不能實現完整迭代。
完整迭代必須是遊標從0開始,遊標到0結束。
6、後記
本文針對Redis的SCAN系列命令做了詳細的對比分析以及實際使用示例,並整理了面試中的高頻問題。建議閱讀本文的同學實際動手練習下,效果更好。歡迎關注@zxiaofan《玩轉Redis》系列文章共同成長。
第5節提到的面試問題,現在能答上幾個了呢?
祝君好運!
Life is all about choices!
將來的你一定會感激現在拼命的自己!
【CSDN】【GitHub】【OSCHINA】【掘金】【微信公衆號】