Redis 實用小技巧——批次刪除指定的 key

快樂的皮拉夫發表於2023-05-08

日常工作當中經常會遇到刪除 Redis key 的問題,如果是刪除某個 key ,使用 DEL {keyname} 或者 EXPIRE {keyname} {ttl} 都可以實現。但如果想要一次性刪除多個 key 應該怎麼處理呢?你可能會想到直接使用 DEL key [key ...] 的方式來處理,但是當要刪除的 key 有很多呢?或者我們事先並不能確認要刪除的是哪些 key 呢?(比如透過搜尋條件匹配出來的 key )

下面我們就來看看如何巧妙地處理這類問題。

場景一:刪除所有的 key

如果需要執行初始化的操作,清理掉資料庫所有的鍵,可以使用 FLUSHDB 或者 FLUSHALL 命令操作。

FLUSHDB:刪除當前資料庫中的所有 key 。
FLUSHALL:刪除當前連線所有資料庫的所有 key 。

場景二:刪除所有滿足匹配條件的 key( key 數量較少或者測試環境)

可以在命令列環境下使用 redis-cli 命令在外部執行 KEYS {pattern} 命令,拿到結果以後透過 xargs 命令傳遞給 DEL 作為輸入引數,進而刪除匹配的 key 。具體命令如下:

redis-cli -h {hostname} -p {port} -a {password} -n {database} --raw keys "{pattern}" | xargs -I {} redis-cli -h {hostname} -p {port} -a {password} -n {database} DEL "{}"

說明

  1. redis-cli 是訪問 Redis 的客戶端命令,用法是:redis-cli [OPTIONS] [cmd [arg [arg ...]]]
  2. hostname:伺服器主機,port:伺服器埠,a:密碼(無密碼可預設),n:資料庫編號。
  3. 低版本的 Redis 會在返回結果中加上數字編號,使用 --raw 引數可以去掉結果編號。
  4. xargs -I {} 引數可以避免 key 中存在空格導致的引數拆分異常問題

但是這種操作是有限制的,主要受限於 KEYS 命令。因為 Redis 6 版本以下都是採用單執行緒處理請求,如果在 key 數量較大的情況下使用 KEYS 命令,會阻塞執行緒,導致其他客戶端無法正常訪問,這在生產環境是不可接受的(基於此原因,很多公司生產環境 KEYS 都是禁用的)。這就是接下來要說的第三種場景。

場景三:刪除所有滿足匹配條件的 key( key 數量較多或者生產環境)

為瞭解決場景二中的 KEYS 命令造成的執行緒阻塞問題,我們可以使用 SCAN 命令來解決。

讓我們先來瞭解一下 SCAN 命令的使用。

SCAN 用於迭代當前資料庫中的資料庫鍵,用法如下:

SCAN cursor [MATCH pattern] [COUNT count]

簡單概括一下: SCAN 命令就是透過遊標的方式分步從資料庫獲取資料,每次以遊標方式進行遍歷(遊標從上一次遍歷結果中返回,初始遊標為 0 ),結果會返回一個新遊標和匹配的鍵集合(返回鍵的數量不不確定,小於等於 COUNT ),如果返回遊標為 0 則視為遍歷結束(不以遍歷結果為空作為結束標識)。可以使用 MATCH 引數匹配模式,COUNT 引數限制返回的鍵的個數。

KEYS 命令相比,SCAN 命令雖然複雜度也是 O(n),但是它是透過遊標分步進行的,不會阻塞執行緒。同時 redis-cli 命令本身支援 --scan--pattern 的引數,可以直接在命令列獲取到匹配的結果。

修改後的命令如下:

redis-cli -h {hostname} -p {port} -a {password} -n {database} --raw --scan --pattern "{pattern}" | xargs -I {} redis-cli -h {hostname} -p {port} -a {password} -n {database} -L 100 DEL "{}"

注意這裡並沒有直接在 redis-cli 中使用 SCAN 命令,而是用 --scan 引數的方式呼叫,這是因為 SCAN 命令會返回兩個引數(遊標和結果),這不利於作為 xargs 的輸入引數,而 --scan 引數只返回匹配的鍵,可以和 xargs 命令完美結合。而且可以給 xargs 指定輸入引數的條數(-L),進一步限制每一次刪除的鍵的個數(在沒有指定 COUNT 引數情況下,預設值是 10 ,SCAN 每次會返回最多 10 個左右的資料,並非嚴格相等)。

到這裡看似問題已經得到了解決,但是在實際場景中這種處理方式還是存在一些問題。

SCAN 命令雖然解決了執行緒阻塞的問題,但是也帶來了效率的問題。假設資料庫 key 的數量級在 10w+ 左右,需要刪除的 key 數量級在 100+ 左右,這時候如果使用上述命令手動操作的話無疑是十分痛苦的,需要不斷地重複執行( SCAN 命令並不是每次都可以返回匹配到的結果集,只要沒有返回 0 遊標,就需要繼續遍歷)。另外一次給 DEL 傳遞過多的引數也不是一種很好的選擇,因為如果 key 比較大時,使用 DEL 刪除本身也會造成執行緒阻塞,這樣整個命令的阻塞時間就取決於 key 的數量和大小。

綜上所述,主要有兩個影響因素需要考慮:一個是如何在不阻塞執行緒的情況下,高效查詢匹配的 key ;另一個是如何避免在執行刪除操作的時候造成執行緒阻塞。

針對第一種情況,可以考慮透過指令碼程式執行 SCAN 命令,這樣就不必擔心重複執行的效率問題了。針對 DEL 命令可能造成的阻塞問題,可以使用 EXPIRE 命令替換。以 PHP 語言為例,可以使用以下指令碼進行處理:

use Predis\Client;

$client = new Client();
while (1) {
    list($iterator, $result) = $client->scan(0, ['MATCH' => 'PHP*', 'COUNT' => 50]);
    foreach ($result as $key) {
        $client->expire($key, mt_rand(0, 600));
    }
    if ($iterator == "0") {
        break;
    }
}

說明:

  1. 這裡使用的是 predis/predis composer 包,安裝方式:composer require predis/predis
  2. 不使用遍歷返回的結果集作為 while 的判斷條件是因為 SCAN 命令結束的標誌是返回值為 0 的遊標。
  3. 使用 EXPIRE 設定過期的時候,過期時間採用了隨機數的方式,是為了防止在刪除 key 的數量過多時,同一時間集中過期引起雪崩現象。

當然,如果需要刪除的 key 和 key 的總數數量級相差太大的話,使用 SCAN 命令遍歷的效率還是差了些。這時可以藉助 BGSAVE 生成 rdb 檔案,然後再透過 rdb分析工具(rdbtools) 獲取需要操作的 key ,藉助程式進行過期處理。這個小技巧我們會在介紹「rdb檔案」的應用場景的時候單獨介紹。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
你應該瞭解真相,真相會讓你自由。

相關文章