1. 程式人生 > 實用技巧 >9. Redis中游標迭代器(scan)

9. Redis中游標迭代器(scan)

楔子

我們說如果想查詢資料庫中都有哪些key的話,那麼可以使用keys命令來檢視,keys後面接一個模式,即可返回所有匹配指定模式的key。並且指定模式的時候,可以使用萬用字元,比如:

  • *:匹配任意多個任意字元
  • ?:匹配單個任意字元
  • [...]:匹配[]中的任意一個字元

當然keys這個命令很簡單,用起來也很方便,但是該命令存在兩個缺點:

  • 此命令沒有分頁功能,我們只能一次性查詢出所有符合條件的 key 值,如果查詢結果非常巨大,那麼得到的輸出資訊也會非常多;
  • keys 命令是遍歷查詢,等於將資料庫中的key和指定的模式一一對比,看是否匹配,因此它的查詢時間複雜度是 o(n),所以資料量越大查詢時間就越長。

並且由於每個Redis例項是使用單執行緒處理所有請求的,故keys命令和其他命令都是在同一個佇列排隊等待執行的,如果keys命令執行時間過長,則會阻塞其他命令的執行,導致效能問題。並且如果keys命令需要匹配非常多的key,不僅輸出資訊多,還可能造成長期停頓。

scan命令

因此為了解決這一點,Redis在2.8版本的時候提出了一個scan命令,主要用於解決keys命令可能導致整個Redis例項停頓的問題。

scan是一種迭代命令,主要是對keys命令進行了分解,即原本使用一個keys請求一次匹配獲取所有符合的key的操作,分解了多次scan操作。每次scan操作返回匹配的key的一個子集,這樣每個scan請求的操作時間很短,多次scan請求之間可以執行其他命令,故減少對其他命令執行的阻塞。當最後一個scan請求發現沒有資料可返回了,則操作完成,彙總該次所有scan請求的資料,從而達到與keys命令一次獲取的資料相同。

由於scan命令需要執行多次,即相當於執行了多個命令,存在多次命令請求和響應週期,故整體執行時間可能要比keys命令長。

生成資料

我們使用Python往Redis裡面新增25個key。

import redis

client = redis.Redis(host="47.94.174.89", decode_responses="utf-8")

# 生成24個key
keys = [f"satori{i}" for i in range(1, 25)]
values= list(range(1, 25))

# 新增
client.mset(dict(zip(keys, values)))

然後我們看一下scan怎麼使用,命令:scan 遊標 match 模式 count 數量,其中的match和count是可選的,我們先來講一下游標。

127.0.0.1:6379> scan 0  # 我們這裡沒有指定match,會匹配所以的key;沒有指定count,預設每次返回10條
1) "14"  # 然後遊標以0開始,返回資料之後,會得到一個新的遊標,然後下次從這個新的遊標開始迭代
2)  1) "satori1"
    2) "satori14"
    3) "satori12"
    4) "satori22"
    5) "satori20"
    6) "satori11"
    7) "satori15"
    8) "satori5"
    9) "satori21"
   10) "satori8"
127.0.0.1:6379> scan 14  # 上一個遊標返回了14,所以第二次從14開始迭代,然後返回10條
1) "23"  # 返回遊標23
2)  1) "satori18"
    2) "satori24"
    3) "satori23"
    4) "satori9"
    5) "satori3"
    6) "satori13"
    7) "satori17"
    8) "satori4"
    9) "satori10"
   10) "satori6"
127.0.0.1:6379> scan 23  # 所以這裡從第二次返回的遊標23 開始迭代
1) "0"  # 如果遊標返回了0,代表迭代結束了,此時所以的key都被迭代過了。
2) 1) "satori19"
   2) "satori16"
   3) "satori7"
   4) "satori2"
127.0.0.1:6379> 

我們講解一下里面的引數:

  • 遊標:游標位置,從0開始,到0結束。如果遊標值返回的不是0的話,儘管查詢結果是空,迭代也依舊沒有結束。
  • match 模式:匹配指定模式的key,類似於keys pattern中的pattern。如果不指定那麼等價於全部匹配
  • count 數量:指定返回的數量,如果不指定,那麼每次返回10條

實際操作一下,不過這裡的key有點多,我們就刪除一部分只保留14個key吧。

127.0.0.1:6379> scan 0 count 11  # 返回11個
1) "7"
2)  1) "satori1"
    2) "satori14"
    3) "satori12"
    4) "satori11"
    5) "satori5"
    6) "satori8"
    7) "satori13"
    8) "satori3"
    9) "satori9"
   10) "satori4"
   11) "satori10"
127.0.0.1:6379> scan 7  # 還剩4個
1) "0"
2) 1) "satori6"
   2) "satori2"
   3) "satori7"
127.0.0.1:6379> 
127.0.0.1:6379> scan 0 count 15  # 直接返回15個
1) "0"  # 游標直接變為0,因為遍歷一次就結束了
2)  1) "satori1"
    2) "satori14"
    3) "satori12"
    4) "satori11"
    5) "satori5"
    6) "satori8"
    7) "satori13"
    8) "satori3"
    9) "satori9"
   10) "satori4"
   11) "satori10"
   12) "satori6"
   13) "satori2"
   14) "satori7"
127.0.0.1:6379> 

匹配模式,這裡選擇以satori1開頭的。

127.0.0.1:6379> scan 0 match satori1*  # 但是這裡沒有遍歷完畢,而是保留了一個
1) "11"
2) 1) "satori1"
   2) "satori14"
   3) "satori12"
   4) "satori11"
   5) "satori13"
127.0.0.1:6379> scan 11 match satori1*  # 所以判斷遍歷是否結束,我們只看它返回的遊標是不是0。
1) "0"
2) 1) "satori10"

使用Python操作Redis中游標

import redis

client = redis.Redis(host="47.94.174.89", decode_responses="utf-8", password=123456)

# 裡面三個引數:分別是cursor、match、count後面兩個預設為None
print(client.scan(0, count=3))  # (2, ['satori1', 'satori14', 'satori12', 'satori11'])

# 會返回一個元組,裡面是遊標和對應的key(一個列表)
# 那麼我們就可以開始遍歷了, 一次遍歷6個吧
cursor = 0
while res := client.scan(cursor, count=6):
    print(res[1])
    if not (cursor := res[0]):
        break
"""
['satori1', 'satori14', 'satori12', 'satori11', 'satori5', 'satori8']
['satori13', 'satori3', 'satori9', 'satori4', 'satori10', 'satori6']
['satori2', 'satori7']
"""

以上就是scan命令,除了scan,還有hscan:檢索字典中的鍵值對、sscan:檢索集合中的元素、zscan:檢索有序集合中的元素(包括成員和分數值),有興趣可以自己嘗試一下。這些命令的使用方式都是一樣的,只不過命令的名字不一樣罷了。