楔子
我們說如果想查詢數據庫中都有哪些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:檢索有序集合中的元素(包括成員和分數值)
,有興趣可以自己嘗試一下。這些命令的使用方式都是一樣的,只不過命令的名字不一樣罷了。