玩轉Redis-如何高效訪問Redis中的海量數據

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】【掘金】【微信公衆號
歡迎訂閱zxiaofan的微信公衆號,掃碼或直接搜索zxiaofan


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章