1. 程式人生 > 資料庫 >redis中scan命令的基本實現方法

redis中scan命令的基本實現方法

前言

在一個天朗氣清的日子,小灰登上了線上的redis打算查詢資料。然而他只記得字首而不知道整個鍵是多少,於是在命令列敲入了“keys xxx*”命令。

瞬間服務卡死,報警郵件堆滿了郵箱,而小灰,只能目瞪狗呆的等待著即將降臨的case study。

基本上,keys *命令都是在線上是被運維禁止的。

redis的鍵在鍵值對大小大於hash-max-ziplist-value且個數小於hash-max-ziplist-entries的時候,是存放在散列表資料結構中的,在執行keys命令的時候,需要遍歷資料庫鍵空間,把所有鍵都取出來後與keys後面的pattern匹配。

在鍵很多的情況下,redis可能的卡頓會在秒級以上,導致所有流量都打到資料庫,使得資料庫雪崩。

那我們怎麼才能夠在查詢到目標鍵呢?在redis2.8.0的時候加入了scan命令,可以分批次掃描redis鍵。雖然在應用的時候會使得要查詢到全部符合要求的key的時間變長,但是大大大大減少了redis卡頓的機率

在這裡先補充一下背景:

redis中的字典rehash是漸進式雜湊,即不是一次性把所有的鍵都遷移到新的雜湊表,而是在下面兩種情況下遷移資料:
每次雜湊表操作的時候,如果當前正在rehash,則遷移一個節點;
服務空閒時,會rehash一百個節點。

scan命令可以保證在(沒有鍵修改的)字典正在rehash的過程中做到以下兩點:

  • 不出現重複資料
  • 不遺漏資料

那scan命令是怎麼做到在rehash過程中都能不重複不遺漏地遍歷所有節點的呢?讓我們來一起走讀一下原始碼。

Let's GO!

在使用scan命令的時候,我們每次傳入一個遊標(從0開始),然後下一輪繼續使用本輪redis返回的遊標。scan字典的核心函式是dictScan,而dictScan的更新遊標的核心程式碼如下:

v |= ~m0;//或者m1
/* Increment the reverse cursor */
v = rev(v);
v++;
v = rev(v);

其中m0、m1為當前雜湊表大小減一,rev是二進位制逆序。

看到這裡,不知道在座的各位是不是也是跟我一樣是下面這個表情

讓我們來模擬一下問題,就清楚了。

我們假設現在在一個四個節點的雜湊表中遍歷,如下圖,遊標的遍歷節點為:0 -> 2 -> 1 -> 3 :

redis中scan命令的基本實現方法

再來模擬8節點的情況:

redis中scan命令的基本實現方法

看到這裡是不是稍微明白了,上面那段程式碼就是在當前的有效位數(比如四節點則有效位數2)範圍內,從左到右進一位。

假設在遍歷了0,返回2之後,字典進行了擴容,則接下來應該訪問 2 -> 6 -> 1 -> 5 -> 3 -> 7。

小灰:咦,那4不是遺漏了嗎?

4已經在第一輪遍歷0的時候,把擴容後的4的資料也訪問了。

所以,假設擴容前有效位為m,因為redis的雜湊表擴容每次都是當前節點滿了( use==size)的時候擴容為大於size的2^N,所以擴容後有效位則為m+1。

上面那段程式碼其實是保持低位的m位不變,高位一個為0一個為1。這樣就保證了擴容後,跳過了的節點已經在之前被訪問過,因為跳過的節點是被訪問過的節點分出來的。

redis中scan命令的基本實現方法

縮容同理,可以自己推一下。

看到這裡,是不是覺得redis的scan遊標設計的很巧妙呢?

小灰:原來如此,看來我又可以去查資料了呢!

最後附上完整的rehash過程中scan的程式碼:

// 指向兩個雜湊表
t0 = &d->ht[0];
t1 = &d->ht[1];

/* Make sure t0 is the smaller and t1 is the bigger table */
// 確保 t0 比 t1 要小
if (t0->size > t1->size) {
 t0 = &d->ht[1];
 t1 = &d->ht[0];
}

// 記錄掩碼
m0 = t0->sizemask;
m1 = t1->sizemask;

/* Emit entries at cursor */
// 指向桶,並迭代桶中的所有節點
de = t0->table[v & m0];
while (de) {//迭代第一張小hash表
 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 {//迭代第二張大hash表
 /* 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;
 }

 //計算一個雜湊表節點索引的方法 是 hash(key)&mask。雜湊表容量為 8,則 mask 為 111,因此,節點的索引值就取決於雜湊值的低 3 bit,
 // 設索引值是 abc。如果雜湊表容量為 16,則 mask 為 1111,該節點的雜湊值不變,而索引值是 ?abc,其中 ? 取 0 或 1 中的一個,
 // 也就是說,該節點在容量為 16 的雜湊表中,索引要麼是 0abc 要麼是 1abc。以此類推,如果雜湊表容量為32,
 // 則該節點的索引可能是 00abc,01abc,10abc 或者 11abc 中的一個。/* Increment the reverse cursor not covered by the smaller mask.*/
 v |= ~m1;//用於保留 v 的低 n 位數,其餘位全置為 1
 //下面這一段,最終得到的新 v,就是向最高位加 1,且向低位方向進位
 v = rev(v);//將 v 的二進位制位進行翻轉,所以,v的低 n 位數成了高 n 位數,並且進行了翻轉
 v++;
 v = rev(v);//再次二進位制翻轉

 /* Continue while bits covered by mask difference is non-zero */
} while (v & (m0 ^ m1));//終止條件是 v的高位區別位沒有1了,其實就是說到頭了。

總結

到此這篇關於redis中scan命令的基本實現方法的文章就介紹到這了,更多相關redis中scan命令實現內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!