關於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 keyTime 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時,要刪除時,如何優雅地刪除大Key?
從Redis2.8版本開始支持SCAN命令,通過m次時間復雜度為O(1)的方式,遍歷包含n個元素的大key.這樣避免單個O(n)的大命令,導致Redis阻塞。
這里刪除大key操作的思想也是如此。
說明:
redis大key,這里指的是大的集合數據類型,如(set/hash/list/sorted set),一個key包含很多元素。
由於redis是單線程,在刪除大key(千萬級別的set集合)的時候,或者清理過期大key數據時,主線程忙於刪除這個大key,會導致redis阻塞、崩潰,應用程序異常的情況。
redis-cli --bigkeys 命令。可以找到某個實例5種數據類型(String、hash、list、set、zset)的最大key 、等其他方式
一個栗子:↓
線上redis作為實時去重的一個工具,里面有6千萬的用戶guid,這么一個set集合,如果直接使用del刪除,會導致redis嚴重阻塞
10.1.254.18:6380> info memory # Memory used_memory:15175740016 used_memory_human:14.13G used_memory_rss:22302339072 used_memory_peak:22351749192 used_memory_peak_human:20.82G used_memory_lua:36864 mem_fragmentation_ratio:1.47 mem_allocator:jemalloc-3.6.0
10.1.254.18:6380> scard helper_2019-03-12 (integer) 64530980
10.1.254.18:6380> del helper_2019-03-12 (integer) 1 (81.23s) 10.1.254.18:6380> info memory # Memory used_memory:8466985704 used_memory_human:7.89G used_memory_rss:10669453312 used_memory_peak:22351749192 used_memory_peak_human:20.82G used_memory_lua:36864 mem_fragmentation_ratio:1.26 mem_allocator:jemalloc-3.6.0
可以看到,helper_2019-03-12這個key,是一個包含64530980個元素的集合,直接使用del刪除命令,花的時間為:81.23s,顯然會發送超時、阻塞,程序異常!
這種情況,應該使用sscan命令,批量刪除set集合元素的方法。下面是一個Java代碼分批刪除redis中set集合的例子:
private static void test2(){ // 連接redis 服務器
Jedis jedis = new Jedis("0.0.0.0",6379); jedis.auth("123456"); // 分批刪除
try { ScanParams scanParams = new ScanParams(); // 每次刪除 500 條
scanParams.count(500); String cursor = ""; while (!cursor.equals("0")){ ScanResult<String> scanResult=jedis.sscan("testset", cursor, scanParams); // 返回0 說明遍歷完成
cursor = scanResult.getStringCursor(); List<String> result = scanResult.getResult(); long t1 = System.currentTimeMillis(); for(int m = 0;m < result.size();m++){ String element = result.get(m); jedis.srem("testset", element); } long t2 = System.currentTimeMillis(); System.out.println("刪除"+result.size()+"條數據,耗時: "+(t2-t1)+"毫秒,cursor:"+cursor); } }catch (JedisException e){ e.printStackTrace(); }finally { if(jedis != null){ jedis.close(); } } }
對於其它集合,也有對應的方法。
- hash key:通過hscan命令,每次獲取500個字段,再用hdel命令;
- set key:使用sscan命令,每次掃描集合中500個元素,再用srem命令每次刪除一個元素;
- list key:刪除大的List鍵,未使用scan命令; 通過ltrim命令每次刪除少量元素。
- sorted set key:刪除大的有序集合鍵,和List類似,使用sortedset自帶的zremrangebyrank命令,每次刪除top 100個元素。
后台刪除之lazyfree機制:
為了解決redis使用del命令刪除大體積的key,或者使用flushdb、flushall刪除數據庫時,造成redis阻塞的情況,在redis 4.0引入了lazyfree機制,可將刪除操作放在后台,讓后台子線程(bio)執行,避免主線程阻塞。
lazy free的使用分為2類:第一類是與DEL命令對應的主動刪除,第二類是過期key刪除、maxmemory key驅逐淘汰刪除。
主動刪除:
UNLINK命令是與DEL一樣刪除key功能的lazy free實現。唯一不同時,UNLINK在刪除集合類鍵時,如果集合鍵的元素個數大於64個(詳細后文),會把真正的內存釋放操作,給單獨的bio來操作。
127.0.0.1:7000> UNLINK mylist (integer) 1 FLUSHALL/FLUSHDB ASYNC 127.0.0.1:7000> flushall async //異步清理實例數據
被動刪除:
lazy free應用於被動刪除中,目前有4種場景,每種場景對應一個配置參數; 默認都是關閉。
lazyfree-lazy-eviction no lazyfree-lazy-expire no lazyfree-lazy-server-del no slave-lazy-flush no
lazyfree-lazy-eviction
針對redis內存使用達到maxmeory,並設置有淘汰策略時;在被動淘汰鍵時,是否采用lazy free機制;
因為此場景開啟lazy free, 可能使用淘汰鍵的內存釋放不及時,導致redis內存超用,超過maxmemory的限制。此場景使用時,請結合業務測試。
lazyfree-lazy-expire
針對設置有TTL的鍵,達到過期后,被redis清理刪除時是否采用lazy free機制;
此場景建議開啟,因TTL本身是自適應調整的速度。
lazyfree-lazy-server-del
針對有些指令在處理已存在的鍵時,會帶有一個隱式的DEL鍵的操作。如rename命令,當目標鍵已存在,redis會先刪除目標鍵,如果這些目標鍵是一個big key,那就會引入阻塞刪除的性能問題。
此參數設置就是解決這類問題,建議可開啟。
slave-lazy-flush
針對slave進行全量數據同步,slave在加載master的RDB文件前,會運行flushall來清理自己的數據場景,
參數設置決定是否采用異常flush機制。如果內存變動不大,建議可開啟。可減少全量同步耗時,從而減少主庫因輸出緩沖區爆漲引起的內存使用增長。
expire及evict優化
redis在空閑時會進入activeExpireCycle循環刪除過期key,每次循環都會率先計算一個執行時間,在循環中並不會遍歷整個數據庫,
而是隨機挑選一部分key查看是否到期,所以有時時間不會被耗盡(采取異步刪除時更會加快清理過期key),剩余的時間就可以交給freeMemoryIfNeeded來執行。