1. 程式人生 > >如何優雅地刪除Redis大鍵

如何優雅地刪除Redis大鍵

關於Redis大鍵(Key),我們從[空間複雜性]和訪問它的[時間複雜度]兩個方面來定義大鍵。
前者主要表示Redis鍵的佔用記憶體大小;後者表示Redis集合資料型別(set/hash/list/sorted set)鍵,所含有的元素個數。以下兩個示例:

1個大小200MB的String鍵(String Object最大512MB);記憶體空間角度佔用較大
1個包含100000000(1kw)個欄位的Hash鍵,對應訪問模式(如hgetall)時間複雜度高

因為記憶體空間複雜性處理耗時都非常小,測試 del 200MB String鍵耗時約1毫秒,
而刪除一個含有1kw個欄位的Hash鍵,卻會阻塞Redis程序數十秒。所以本文只從時間複雜度分析大的集合類鍵。刪除這種大鍵的風險,以及怎麼優雅地刪除。

在Redis叢集中,應用程式儘量避免使用大鍵;直接影響容易導致叢集的容量和請求出現”傾斜問題“,具體分析見文章:redis-cluster-imbalance。但在實際生產過程中,總會有業務使用不合理,出現這類大鍵;當DBA發現後推進業務優化改造,然後刪除這個大鍵;如果直接刪除它,DEL命令可能阻塞Redis程序數十秒,對應用程式和Redis叢集可用性造成嚴重的影響。

直接刪除大Key的風險

DEL命令在刪除單個集合型別的Key時,命令的時間複雜度是O(M),其中M是集合型別Key包含的元素個數。

DEL key
Time complexity: O(N) where N is the number of keys that will be removed. When a key to remove holds a value other than a string, the individual complexity for this key is O(M) where M is the number of elements in the list, set, sorted set or hash. Removing a single key that holds a string value is O(1).

生產環境中遇到過多次因業務刪除大Key,導致Redis阻塞,出現故障切換和應用程式雪崩的故障。
測試刪除集合型別大Key耗時,一般每秒可清理100w~數百w個元素; 如果數千w個元素的大Key時,會導致Redis阻塞上10秒
可能導致叢集判斷Redis已經故障,出現故障切換;或應用程式出現雪崩的情況。

說明:Redis是單執行緒處理。
單個耗時過大命令,導致阻塞其他命令,容易引起應用程式雪崩或Redis叢集發生故障切換。
所以避免在生產環境中使用耗時過大命令。

Redis刪除大的集合鍵的耗時, 測試估算,可參考;和硬體環境、Redis版本和負載等因素有關

Key型別 Item數量 耗時
Hash ~100萬 ~1000ms
List ~100萬 ~1000ms
Set ~100萬 ~1000ms
Sorted Set ~100萬 ~1000ms

當我們發現叢集中有大key時,要刪除時,如何優雅地刪除大Key?

如何優雅地刪除各類大Key

從Redis2.8版本開始支援SCAN命令,通過m次時間複雜度為O(1)的方式,遍歷包含n個元素的大key.
這樣避免單個O(n)的大命令,導致Redis阻塞。 這裡刪除大key操作的思想也是如此。

Delete Large Hash Key

通過hscan命令,每次獲取500個欄位,再用hdel命令,每次刪除1個欄位。
Python程式碼:

12345678 def del_large_hash(): r = redis.StrictRedis(host='redis-host1', port=6379) large_hash_key ="xxx" #要刪除的大hash鍵名 cursor = '0' while cursor != 0: cursor, data = r.hscan(large_hash_key, cursor=cursor, count=500) for item in data.items(): r.hdel(large_hash_key, item[0])

Delete Large Set Key

刪除大set鍵,使用sscan命令,每次掃描集合中500個元素,再用srem命令每次刪除一個鍵
Python程式碼:

12345678 def del_large_set(): r = redis.StrictRedis(host='redis-host1', port=6379) large_set_key = 'xxx' # 要刪除的大set的鍵名 cursor = '0' while cursor != 0: cursor, data = r.sscan(large_set_key, cursor=cursor, count=500) for item in data: r.srem(large_size_key, item)

Delete Large List Key

刪除大的List鍵,未使用scan命令; 通過ltrim命令每次刪除少量元素。
Python程式碼:

12345 def del_large_list(): r = redis.StrictRedis(host='redis-host1', port=6379) large_list_key = 'xxx' #要刪除的大list的鍵名 while r.llen(large_list_key)>0: r.ltrim(large_list_key, 0, -101) #每次只刪除最右100個元素

Delete Large Sorted set key

刪除大的有序集合鍵,和List類似,使用sortedset自帶的zremrangebyrank命令,每次刪除top 100個元素。
Python程式碼:

12345 def del_large_sortedset(): r = redis.StrictRedis(host='large_sortedset_key', port=6379) large_sortedset_key='xxx' while r.zcard(large_sortedset_key)>0: r.zremrangebyrank(large_sortedset_key,0,99)#時間複雜度更低 , 每次刪除O(log(N)+100)

Redis Lazy Free

應該從3.4版本開始,Redis會支援lazy delete free的方式,刪除大鍵的過程不會阻塞正常請求。