【臨實戰】使用 Python 從 Redis 中刪除 4000W 個 KEY

臨書發表於2019-03-04
本文主要涉及 Redis 的以下兩個操作和其 Python 實現,目錄:
  • SCAN 命令
  • DEL 命令
  • 使用 Python SCAN
  • 使用 Python DEL
  • 成果展示

SCAN 命令

SCAN 命令及相關的 SSCAN、HSCAN 和 ZSCAN 命令都用於增量迭代(incrementally iterate)一個集合的元素(a collection of elements):
  • SCAN 用於迭代當前資料庫中的資料庫鍵
  • SSCAN 用於迭代集合鍵中的元素
  • HSCAN 用於迭代雜湊鍵中的鍵值對
  • ZSCAN 用於迭代有序集合中的元素(包括元素分值和元素分值)
以上四列命令都支援增量迭代,每次執行都會返回少量元素,所以他們都可以用於生產環境,而不會出現像 KEYS、SMEMBERS 命令一樣 — 可能會阻塞伺服器
不過,增量式迭代命令也不是沒有缺點的:
舉個例子,使用 SMEMBERS 命令可以返回集合鍵當前包含的所有元素,但是對於 SCAN 這類增量迭代命令來說,因為在堆鍵進行增量迭代的過程中,鍵可能會被改變,所以增量式迭代命令只能對被返回的元素提供有限的保證(offer limited guarantees about the returned elements)。
因為 SCAN、SSCAN、HSCAN 和 ZSCAN 命令的工作方式都非常相似,但是要記住:
  • SSCAN、HSCAN 和 ZSCAN 命令的第一個引數總是一個資料庫鍵;
  • SCAN 命令則不需要在第一個引數提供任何資料庫鍵 — 因為它迭代的是當前資料庫中的所有資料庫鍵。
SCAN 命令的基本用法
SCAN 命令是一個基於遊標的迭代器(cursor based iterator):
SCAN 命令每次被呼叫後,都會向使用者返回一個新的遊標,使用者在下次迭代時需要使用這個新遊標作為 SCAN 命令的遊標引數,以此來延續之前的迭代過程。
當 SCAN 命令的遊標引數被設定為 0 時,伺服器開始一次新的迭代,而當伺服器向使用者返回值為 0 的遊標時,表示迭代結束。
示例:
redis 127.0.0.1:6379> scan 0
1) "17"
2)  1) "key:12"
    2) "key:8"
    3) "key:4"
    4) "key:14"
    5) "key:16"
    6) "key:17"
    7) "key:15"
    8) "key:10"
    9) "key:3"
    10) "key:7"
    11) "key:1"

redis 127.0.0.1:6379> scan 17
1) "0"
2) 1) "key:5"
   2) "key:18"
   3) "key:0"
   4) "key:2"
   5) "key:19"
   6) "key:13"
   7) "key:6"
   8) "key:9"
   9) "key:11"複製程式碼
上面的例子中,第一次迭代用 0 作為遊標,表示開始第一次迭代。
第二次迭代使用第一次迭代時返回的遊標,即:17。
從示例可以看出,SCAN 命令的返回是一個兩個元素的陣列,第一個元素是新遊標,第二個元素也是一個陣列,包含有所被包含的元素。
第二次呼叫 SCAN 命令時,返回遊標 0,這表示迭代已經結束了,整個資料集(collection)已經被完整遍歷過一遍了。
這個過程被稱為一次完整遍歷(full iteration)。
精簡一下內容,補充三點:
  1. 因為 SCAN 命令僅僅使用遊標來記錄迭代狀態,所以在迭代過程中,如果這個資料集的元素有增減,如果是減,不保證元素不返回;如果是增,也不保證一定返回;而且在某種情況下同一個元素還可能被返回多次。所以對迭代返回的元素所執行的操作最好可以重複執行多次(冪等)。
  2. 增量迭代命令不保證每次迭代所返回的元素數量(沒掃到嘛),但是我們可以使用 COUNT 選項對命令的行為進行一定程度的調整。COUNT 引數的預設值為 10,在迭代一個足夠大的、由雜湊表實現的資料庫、集合鍵、雜湊鍵或者有序集合鍵時,如果使用者沒有使用 MATCH 選項,那麼命令返回的數量通常和 COUNT 選項指定的一樣,或者多一些(?),在迭代編碼為整數集合(intset:一個由整數值構成的小集合)或編碼為壓縮列表(ziplist:由不同值構成的一個小雜湊或者一個小有序集合)時,會無視 COUNT 選項指定的值,在第一次迭代就將資料集的所有元素都返回給使用者。
  3. MATCH 選項,直接看示例吧,如下
示例:
redis 127.0.0.1:6379> sadd myset 1 2 3 foo foobar feelsgood
(integer) 6

redis 127.0.0.1:6379> sscan myset 0 match f*
1) "0"
2) 1) "foo"
   2) "feelsgood"
   3) "foobar"複製程式碼
注意:對元素的模式匹配工作是在命令從資料集中取出元素之後,向客戶端返回元素之前進行的,所以有可能返回空
示例:
redis 127.0.0.1:6379> scan 0 MATCH *11*
1) "288"
2) 1) "key:911"

redis 127.0.0.1:6379> scan 288 MATCH *11*
1) "224"
2) (empty list or set)

redis 127.0.0.1:6379> scan 224 MATCH *11*
1) "80"
2) (empty list or set)

redis 127.0.0.1:6379> scan 80 MATCH *11*
1) "176"
2) (empty list or set)

redis 127.0.0.1:6379> scan 176 MATCH *11* COUNT 1000
1) "0"
2)  1) "key:611"
    2) "key:711"
    3) "key:118"
    4) "key:117"
    5) "key:311"
    6) "key:112"
    7) "key:111"
    8) "key:110"
    9) "key:113"
   10) "key:211"
   11) "key:411"
   12) "key:115"
   13) "key:116"
   14) "key:114"
   15) "key:119"
   16) "key:811"
   17) "key:511"
   18) "key:11"複製程式碼
注意:最後一次迭代,通過 COUNT 選項指定為 1000 強制命令為本次迭代掃描更多元素,從而使返回的元素也變多了。

DEL 命令

這個比較簡單,刪除給定的一個或者多個 key
redis> SET name "redis"
OK
redis> SET type "key-value store"
OK
redis> SET website "redis.com"
OK
redis> DEL name type website
(integer) 3複製程式碼

使用 Python SCAN

安裝 redis 包
pip install redis複製程式碼
完整程式碼示例:
import redis

pool=redis.ConnectionPool(host=`redis_hostname`, port=6379, max_connections=100)
r = redis.StrictRedis(connection_pool=pool)

cursor_number, keys = r.execute_command(`scan`, 0, "count", 200000)

while True:
    if cursor_number == 0:
        # 結束一次完整的比遍歷
        break
    cursor_number, keys = r.execute_command(`scan`, cursor_number, "count", 200000)
    # do something with keys

複製程式碼
我將需要刪除的 key 存在一個檔案裡,有 2.2G,大概 4000W 個,下一步就是刪除了

使用 Python DEL

因為檔案很大,我們用到一個小技巧,分塊讀取
with open("/data/rediskeys") as kf:
    lines = kf.readlines(1024*1024)複製程式碼
呼叫 delete 方法時,用到一個小技巧就是『*』星號
r.delete(*taskkey_list)複製程式碼
我們看一下定義就清楚了:
【臨實戰】使用 Python 從 Redis 中刪除 4000W 個 KEY
delete method
放上完整程式碼:
import redis
import time

pool=redis.ConnectionPool(host=`redis_hostname`, port=6379, max_connections=100)
r = redis.StrictRedis(connection_pool=pool)

start_time = time.time()
SUCCESS_DELETED = 0

with open("/data/rediskeys") as kf:
    while True:
        lines = kf.readlines(1024*1024)
        if not lines:
            break
        else:
            taskkey_list = [i.strip() for i in lines if i.startswith("UCS:TASKKEY")]
            SUCCESS_DELETED += r.delete(*taskkey_list)

        print SUCCESS_DELETED

end_time = time.time()
print end_time - start_time, SUCCESS_DELETED複製程式碼

成果展示

結束,下篇再見
【臨實戰】使用 Python 從 Redis 中刪除 4000W 個 KEY

相關文章