SCAN命令能夠爲用戶保證:從完整遍歷開始直到完整遍歷結束期間,一直存在於數據集內的全部元素都會被完整遍歷返回,可是同一個元素可能會被返回屢次。若是一個元素是在迭代過程當中被添加到數據集的,又或者是在迭代過程當中從數據集中被刪除的,那麼這個元素可能會被返回,也可能不會返回。redis
這是如何實現的呢,先從Redis中的字典dict開始。Redis的數據庫是使用dict做爲底層實現的。算法
Redis中的字典由dict.h/dict
結構表示:shell
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
字典由兩個哈希表dictht
構成,主要用作rehash,日常主要使用ht[0]哈希表。數據庫
哈希表由一個成員爲dictEntry
的數組構成,size
屬性記錄了數組的大小,used
屬性記錄了已有節點的數量,sizemask
屬性的值等於size - 1
。數組大小通常是2n,因此sizemask
二進制是0b11111...
,主要用做掩碼,和哈希值一塊兒決定key應該放在數組的哪一個位置。數組
求key在數組中的索引的計算方法以下:spa
index = hash & d->ht[table].sizemask;
也就是根據掩碼求低位值。code
字典rehash時會使用兩個哈希表,首先爲ht[1]分配空間,若是是擴展操做,ht[1]的大小爲第一個大於等於2倍ht[0].used
的2n,若是是收縮操做,ht[1]的大小爲第一個大於等於ht[0].used
的2n。而後將ht[0]的全部鍵值對rehash到ht[1]中,最後釋放ht[0],將ht[1]設置爲ht[0],新建立一個空白哈希表當作ht[1]。rehash不是一次完成的,而是分屢次、漸進式地完成。索引
舉個例子,如今將一個size爲4的哈希表ht[0]
(sizemask爲11, index = hash & 0b11)rehash至一個size爲8的哈希表ht[1]
(sizemask爲111, index = hash & 0b111)。rem
ht[0]中處於bucket0位置的key的哈希值低兩位爲00
,那麼rehash至ht[1]時index取低三位可能爲000(0)
和100(4)
。也就是ht[0]中bucket0中的元素rehash以後分散於ht[1]的bucket0與bucket4,以此類推,對應關係爲:源碼
ht[0] -> ht[1] ---------------- 0 -> 0,4 1 -> 1,5 2 -> 2,6 3 -> 3,7
若是SCAN命令採起0->1->2->3的順序進行遍歷,就會出現以下問題:
SCAN
命令的遍歷順序,能夠舉一個例子看一下:
127.0.0.1:6379[3]> keys *
1) "bar"
2) "qux"
3) "baz"
4) "foo"
127.0.0.1:6379[3]> scan 0 count 1
1) "2"
2) 1) "bar"
127.0.0.1:6379[3]> scan 2 count 1
1) "1"
2) 1) "foo"
127.0.0.1:6379[3]> scan 1 count 1
1) "3"
2) 1) "qux"
2) "baz"
127.0.0.1:6379[3]> scan 3 count 1
1) "0"
2) (empty list or set)
能夠看出順序是0->2->1->3
,很難看出規律,轉換成二進制觀察一下:
00 -> 10 -> 01 -> 11
二進制就很明瞭了,遍歷採用的順序也是加法,但每次是高位加1的,也就是從左往右相加、從高到低進位的。
SCAN
遍歷字典的源碼在dict.c/dictScan
,分兩種狀況,字典不在進行rehash或者正在進行rehash。
不在進行rehash時,遊標是這樣計算的:
m0 = t0->sizemask;
// 將遊標的umask位的bit都置爲1
v |= ~m0;
// 反轉游標
v = rev(v);
// 反轉後+1,達到高位加1的效果
v++;
// 再次反轉復位
v = rev(v);
當size爲4時,sizemask爲3(00000011),遊標計算過程:
v |= ~m0 v = rev(v) v++ v = rev(v) 00000000(0) -> 11111100 -> 00111111 -> 01000000 -> 00000010(2) 00000010(2) -> 11111110 -> 01111111 -> 10000000 -> 00000001(1) 00000001(1) -> 11111101 -> 10111111 -> 11000000 -> 00000011(3) 00000011(3) -> 11111111 -> 11111111 -> 00000000 -> 00000000(0)
遍歷size爲4時的遊標狀態轉移爲0->2->1->3
。
同理,size爲8時的遊標狀態轉移爲0->4->2->6->1->5->3->7
,也就是000->100->010->110->001->101->011->111
。
再結合前面的rehash:
ht[0] -> ht[1] ---------------- 0 -> 0,4 1 -> 1,5 2 -> 2,6 3 -> 3,7
能夠看出,當size由小變大時,全部原來的遊標都能在大的哈希表中找到相應的位置,而且順序一致,不會重複讀取而且不會遺漏。
當size由大變小的狀況,假設size由8變爲了4,分兩種狀況,一種是遊標爲0,2,1,3
中的一種,此時繼續讀取,也不會遺漏和重複。
但若是遊標返回的不是這四種,例如返回了7,7&11以後變爲了3,因此會從size爲4的哈希表的bucket3開始繼續遍歷,而bucket3包含了size爲8的哈希表中的bucket3與bucket7,因此會形成重複讀取size爲8的哈希表中的bucket3的狀況。
因此,redis裏rehash從小到大時,SCAN命令不會重複也不會遺漏。而從大到小時,有可能會形成重複但不會遺漏。
當正在進行rehash時,遊標計算過程:
/* Make sure t0 is the smaller and t1 is the bigger table */
if (t0->size > t1->size) {
t0 = &d->ht[1];
t1 = &d->ht[0];
}
m0 = t0->sizemask;
m1 = t1->sizemask;
/* Emit entries at cursor */
if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
de = t0->table[v & m0];
while (de) {
next = de->next;
fn(privdata, de);
de = next;
}
/* Iterate over indices in larger table that are the expansion
* of the index pointed to by the cursor in the smaller table */
do {
/* Emit entries at cursor */
if (bucketfn) bucketfn(privdata, &t1->table[v & m1]);
de = t1->table[v & m1];
while (de) {
next = de->next;
fn(privdata, de);
de = next;
}
/* Increment the reverse cursor not covered by the smaller mask.*/
v |= ~m1;
v = rev(v);
v++;
v = rev(v);
/* Continue while bits covered by mask difference is non-zero */
} while (v & (m0 ^ m1));
算法會保證t0是較小的哈希表,不是的話t0與t1互換,先遍歷t0中游標所在的bucket,而後再遍歷較大的t1。
求下一個遊標的過程基本相同,只是把m0
換成了rehash以後的哈希表的m1
,同時還加了一個判斷條件:
v & (m0 ^ m1)
size4的m0爲00000011
,size8的m1爲00000111
,m0 ^ m1
取值爲00000100
,即取兩者mask的不一樣位,看遊標在這些標誌位是否爲1。
假設遊標返回了2,而且正在進行rehash,此時size由4變成了8,兩者mask的不一樣位是低第三位。
首先遍歷t0中的bucket2,而後遍歷t1中的bucket2,公式計算出的下一個遊標爲6(00000110),低第三位爲1,繼續循環,遍歷t1中的bucket6,而後計算遊標爲1,結束循環。
因此正在rehash時,是兩個哈希表都遍歷的,以免遺漏的狀況。