keys命令
keys * 、keys id:* 分別是查詢全部的key以及查詢前綴為id:的key。
缺點:
1、沒有 offset、limit 參數,一次返回所有滿足條件的 key。
2.keys算法是遍歷算法,復雜度是O(n),也就是數據越多,時間復雜度越高。
3.數據量達到幾百萬,keys這個指令就會導致 Redis 服務卡頓,因為 Redis 是單線程程序,順序執行所有指令,其它指令必須等到當前的 keys 指令執行完了才可以繼續。
scan命令
那我們如何去遍歷大數據量呢?我們可以采用redis的另一個命令scan。我們看一下scan的特點
-
復雜度雖然也是 O(n),但是它是通過游標分步進行的,不會阻塞線程
-
提供 count 參數,不是結果數量,是redis單次遍歷字典槽位數量(約等於)
-
同 keys 一樣,它也提供模式匹配功能;
-
服務器不需要為游標保存狀態,游標的唯一狀態就是 scan 返回給客戶端的游標整數;
-
返回的結果可能會有重復,需要客戶端去重復,這點非常重要;
-
單次返回的結果是空的並不意味着遍歷結束,而要看返回的游標值是否為零
scan命令格式
SCAN cursor [MATCH pattern] [COUNT count]
命令解釋:scan 游標 MATCH <返回和給定模式相匹配的元素> count 每次迭代所返回的元素數量
SCAN命令是增量的循環,每次調用只會返回一小部分的元素。所以不會讓redis假死。
SCAN命令返回的是一個游標,從0開始遍歷,到0結束遍歷。
redis > scan 0 match user_token* count 5
1) "6"
2) 1) "user_token:1000"
2) "user_token:1001"
3) "user_token:1010"
4) "user_token:2300"
5) "user_token:1389"
從0開始遍歷,返回了游標6,又返回了數據,繼續scan遍歷,就要從6開始
redis > scan 6 match user_token* count 5
1) "10"
2) 1) "user_token:3100"
2) "user_token:1201"
3) "user_token:1410"
4) "user_token:5300"
5) "user_token:3389"
Redis的結構
Redis使用了Hash表作為底層實現,原因不外乎高效且實現簡單。說到Hash表,很多Java程序員第一反應就是HashMap。沒錯,Redis底層key的存儲結構就是類似於HashMap那樣數組+鏈表的結構。其中第一維的數組大小為2n(n>=0)。每次擴容數組長度擴大一倍。
scan命令就是對這個一維數組進行遍歷。每次返回的游標值也都是這個數組的索引。limit參數表示遍歷多少個數組的元素,將這些元素下掛接的符合條件的結果都返回。因為每個元素下掛接的鏈表大小不同,所以每次返回的結果數量也就不同。
SCAN的遍歷順序
關於scan命令的遍歷順序,我們可以用一個小栗子來具體看一下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
127.0.0.1:6379> keys *
1)
"db_number"
2)
"key1"
3)
"myKey"
127.0.0.1:6379> scan 0 MATCH * COUNT 1
1)
"2"
2) 1)
"db_number"
127.0.0.1:6379> scan 2 MATCH * COUNT 1
1)
"1"
2) 1)
"myKey"
127.0.0.1:6379> scan 1 MATCH * COUNT 1
1)
"3"
2) 1)
"key1"
127.0.0.1:6379> scan 3 MATCH * COUNT 1
1)
"0"
2) (empty list or
set
)
|
我們的Redis中有3個key,我們每次只遍歷一個一維數組中的元素。如上所示,SCAN命令的遍歷順序是
0->2->1->3
這個順序看起來有些奇怪。我們把它轉換成二進制就好理解一些了。
00->10->01->11
我們發現每次這個序列是高位加1的。普通二進制的加法,是從右往左相加、進位。而這個序列是從左往右相加、進位的。這一點我們在redis的源碼中也得到印證。
在dict.c文件的dictScan函數中對游標進行了如下處理
1
2
3
|
v = rev(v);
v++;
v = rev(v);
|
意思是,將游標倒置,加一后,再倒置,也就是我們所說的“高位加1”的操作。
這里大家可能會有疑問了,為什么要使用這樣的順序進行遍歷,而不是用正常的0、1、2……這樣的順序呢,這是因為需要考慮遍歷時發生字典擴容與縮容的情況(不得不佩服開發者考慮問題的全面性)。
我們來看一下在SCAN遍歷過程中,發生擴容時,遍歷會如何進行。加入我們原始的數組有4個元素,也就是索引有兩位,這時需要把它擴充成3位,並進行rehash。
原來掛接在xx下的所有元素被分配到0xx和1xx下。在上圖中,當我們即將遍歷10時,dict進行了rehash,這時,scan命令會從010開始遍歷,而000和100(原00下掛接的元素)不會再被重復遍歷。
再來看看縮容的情況。假設dict從3位縮容到2位,當即將遍歷110時,dict發生了縮容,這時scan會遍歷10。這時010下掛接的元素會被重復遍歷,但010之前的元素都不會被重復遍歷了。所以,縮容時還是可能會有些重復元素出現的。
Redis的rehash
rehash是一個比較復雜的過程,為了不阻塞Redis的進程,它采用了一種漸進式的rehash的機制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/* 字典 */
typedef
struct
dict {
// 類型特定函數
dictType *type;
// 私有數據
void
*privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 當 rehash 不在進行時,值為 -1
int
rehashidx;
/* rehashing not in progress if rehashidx == -1 */
// 目前正在運行的安全迭代器的數量
int
iterators;
/* number of iterators currently running */
} dict
|
在Redis的字典結構中,有兩個hash表,一個新表,一個舊表。在rehash的過程中,redis將舊表中的元素逐步遷移到新表中,接下來我們看一下dict的rehash操作的源碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
int
dictRehash(dict *d,
int
n) {
int
empty_visits = n*10;
/* Max number of empty buckets to visit. */
if
(!dictIsRehashing(d))
return
0;
while
(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert
(d->ht[0].size > (unsigned
long
)d->rehashidx);
while
(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if
(--empty_visits == 0)
return
1;
}
de = d->ht[0].table[d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
while
(de) {
uint64_t h;
nextde = de->next;
/* Get the index in the new hash table */
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
if
(d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return
0;
}
/* More to rehash... */
return
1;
}
|
通過注釋我們就能了解到,rehash的過程是以bucket為基本單位進行遷移的。所謂的bucket其實就是我們前面所提到的一維數組的元素。每次遷移一個列表。下面來解釋一下這段代碼。
- 首先判斷一下是否在進行rehash,如果是,則繼續進行;否則直接返回。
- 接着就是分n步開始進行漸進式rehash。同時還判斷是否還有剩余元素,以保證安全性。
- 在進行rehash之前,首先判斷要遷移的bucket是否越界。
- 然后跳過空的bucket,這里有一個empty_visits變量,表示最大可訪問的空bucket的數量,這一變量主要是為了保證不過多的阻塞Redis。
- 接下來就是元素的遷移,將當前bucket的全部元素進行rehash,並且更新兩張表中元素的數量。
- 每次遷移完一個bucket,需要將舊表中的bucket指向NULL。
- 最后判斷一下是否全部遷移完成,如果是,則收回空間,重置rehash索引,否則告訴調用方,仍有數據未遷移。
由於Redis使用的是漸進式rehash機制,因此,scan命令在需要同時掃描新表和舊表,將結果返回客戶端。
refer: