前言
在一個天朗氣清的日子,小灰登上了線上的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,則遷移一個節(jié)點;
服務空閑時,會rehash一百個節(jié)點。
scan命令可以保證在(沒有鍵修改的)字典正在rehash的過程中做到以下兩點:
那scan命令是怎么做到在rehash過程中都能不重復不遺漏地遍歷所有節(jié)點的呢?讓我們來一起走讀一下源碼。
Let's GO!
在使用scan命令的時候,我們每次傳入一個游標(從0開始),然后下一輪繼續(xù)使用本輪redis返回的游標。scan字典的核心函數是dictScan,而dictScan的更新游標的核心代碼如下:
v |= ~m0;//或者m1
/* Increment the reverse cursor */
v = rev(v);
v++;
v = rev(v);
其中m0、m1為當前哈希表大小減一,rev是二進制逆序。
看到這里,不知道在座的各位是不是也是跟我一樣是下面這個表情
讓我們來模擬一下問題,就清楚了。
我們假設現在在一個四個節(jié)點的哈希表中遍歷,如下圖,游標的遍歷節(jié)點為:0 -> 2 -> 1 -> 3 :
![](http://img.jbzj.com/file_images/article/202010/2020104103429302.png?202094103445)
再來模擬8節(jié)點的情況:
![](http://img.jbzj.com/file_images/article/202010/2020104103501091.jpg?202094103551)
看到這里是不是稍微明白了,上面那段代碼就是在當前的有效位數(比如四節(jié)點則有效位數2)范圍內,從左到右進一位。
假設在遍歷了0,返回2之后,字典進行了擴容,則接下來應該訪問 2 -> 6 -> 1 -> 5 -> 3 -> 7。
小灰:咦,那4不是遺漏了嗎?
4已經在第一輪遍歷0的時候,把擴容后的4的數據也訪問了。
所以,假設擴容前有效位為m,因為redis的哈希表擴容每次都是當前節(jié)點滿了( use==size)的時候擴容為大于size的2^N,所以擴容后有效位則為m+1。
上面那段代碼其實是保持低位的m位不變,高位一個為0一個為1。這樣就保證了擴容后,跳過了的節(jié)點已經在之前被訪問過,因為跳過的節(jié)點是被訪問過的節(jié)點分出來的。
![](http://img.jbzj.com/file_images/article/202010/2020104103633750.png?202094103643)
縮容同理,可以自己推一下。
看到這里,是不是覺得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 */
// 指向桶,并迭代桶中的所有節(jié)點
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;
}
//計算一個哈希表節(jié)點索引的方法 是 hash(key)mask。哈希表容量為 8,則 mask 為 111,因此,節(jié)點的索引值就取決于哈希值的低 3 bit,
// 設索引值是 abc。如果哈希表容量為 16,則 mask 為 1111,該節(jié)點的哈希值不變,而索引值是 ?abc,其中 ? 取 0 或 1 中的一個,
// 也就是說,該節(jié)點在容量為 16 的哈希表中,索引要么是 0abc 要么是 1abc。以此類推,如果哈希表容量為32,
// 則該節(jié)點的索引可能是 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的高位區(qū)別位沒有1了,其實就是說到頭了。
總結
到此這篇關于redis中scan命令的基本實現方法的文章就介紹到這了,更多相關redis中scan命令實現內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
您可能感興趣的文章:- redis哨兵常用命令和監(jiān)控示例詳解
- Redis遍歷所有key的兩個命令(KEYS 和 SCAN)
- PHP操作Redis常用命令的實例詳解
- php操作redis命令及代碼實例大全
- Redis常用數據類型命令實例匯總
- 詳解centos7 yum安裝redis及常用命令
- 查看Redis內存信息的命令
- Redis的KEYS 命令千萬不能亂用
- 詳解Redis基本命令與使用場景