1、什麼是bigkey
在Redis中,一個字符串最大512MB,一個二級數據結構(例如hash、list、set、zset)能夠存儲大約40億個(2^32-1)個元素,但實際上中若是下面兩種狀況,我就會認爲它是bigkey。java
- 字符串類型:它的big體如今單個value值很大,通常認爲超過10KB就是bigkey。
- 非字符串類型:哈希、列表、集合、有序集合,它們的big體如今元素個數太多。
2、危害
bigkey能夠說就是Redis的老鼠屎,具體表如今:redis
1.內存空間不均勻
這樣會不利於集羣對內存的統一管理,存在丟失數據的隱患。數據庫
2.超時阻塞
因爲Redis單線程的特性,操做bigkey的一般比較耗時,也就意味着阻塞Redis可能性越大,這樣會形成客戶端阻塞或者引發故障切換,它們一般出如今慢查詢中。json
例如,在Redis發現了這樣的key,你就等着DBA找你吧。數組
127.0.0.1:6379> hlen big:hash(integer) 2000000127.0.0.1:6379> hgetall big:hash 1) "a" 2) "1"
3.網絡擁塞
bigkey也就意味着每次獲取要產生的網絡流量較大,假設一個bigkey爲1MB,客戶端每秒訪問量爲1000,那麼每秒產生1000MB的流量,對於普通的千兆網卡(按照字節算是128MB/s)的服務器來講簡直是滅頂之災,並且通常服務器會採用單機多實例的方式來部署,也就是說一個bigkey可能會對其餘實例形成影響,其後果不堪設想。緩存
4.過時刪除
有個bigkey,它安分守己(只執行簡單的命令,例如hget、lpop、zscore等),但它設置了過時時間,當它過時後,會被刪除,若是沒有使用Redis 4.0的過時異步刪除(lazyfree-lazy-expire yes),就會存在阻塞Redis的可能性,並且這個過時刪除不會從主節點的慢查詢發現(由於這個刪除不是客戶端產生的,是內部循環事件,能夠從latency命令中獲取或者從slave節點慢查詢發現)。服務器
5.遷移困難
當須要對bigkey進行遷移(例如Redis cluster的遷移slot),其實是經過migrate命令來完成的,migrate其實是經過dump + restore + del三個命令組合成原子命令完成,若是是bigkey,可能會使遷移失敗,並且較慢的migrate會阻塞Redis。網絡
3、怎麼產生的?
通常來講,bigkey的產生都是因爲程序設計不當,或者對於數據規模預料不清楚形成的,來看幾個:數據結構
(1) 社交類:粉絲列表,若是某些明星或者大v不精心設計下,必是bigkey。異步
(2) 統計類:例如按天存儲某項功能或者網站的用戶集合,除非沒幾我的用,不然必是bigkey。
(3) 緩存類:將數據從數據庫load出來序列化放到Redis裏,這個方式很是經常使用,但有兩個地方須要注意:
- 第一,是否是有必要把全部字段都緩存
- 第二,有沒有相關關聯的數據
例如遇到過一個例子,該同窗將某明星一個專輯下全部視頻信息都緩存一個巨大的json中,形成這個json達到6MB,後來這個明星發了一個官宣
4、如何發現
1. redis-cli --bigkeys
redis-cli提供了--bigkeys來查找bigkey,例以下面就是一次執行結果:
-------- summary ------- Biggest string found 'user:1' has 5 bytes Biggest list found 'taskflow:175448' has 97478 items Biggest set found 'redisServerSelect:set:11597' has 49 members Biggest hash found 'loginUser:t:20180905' has 863 fields Biggest zset found 'hotkey:scan:instance:zset' has 3431 members 40 strings with 200 bytes (00.00% of keys, avg size 5.00) 2747619 lists with 14680289 items (99.86% of keys, avg size 5.34) 2855 sets with 10305 members (00.10% of keys, avg size 3.61) 13 hashs with 2433 fields (00.00% of keys, avg size 187.15) 830 zsets with 14098 members (00.03% of keys, avg size 16.99)
能夠看到--bigkeys給出了每種數據結構的top 1 bigkey,同時給出了每種數據類型的鍵值個數以及平均大小。
bigkeys對問題的排查很是方便,可是在使用它時候也有幾點須要注意:
- 建議在從節點執行,由於--bigkeys也是經過scan完成的。
- 建議在節點本機執行,這樣能夠減小網絡開銷。
- 若是沒有從節點,可使用--i參數,例如(--i 0.1 表明100毫秒執行一次)
- --bigkeys只能計算每種數據結構的top1,若是有些數據結構很是多的bigkey,也搞不定,畢竟不是本身寫的東西嘛
- debug object
再來看一個場景:
你好,麻煩幫我查一下Redis裏大於10KB的全部key
您好,幫忙查一下Redis中長度大於5000的hash key
是否是發現用--bigkeys不行了(固然若是改源碼也不是太難),但有沒有更快捷的方法,Redis提供了debug object ${key}命令獲取鍵值的相關信息:
127.0.0.1:6379> hlen big:hash (integer) 5000000 127.0.0.1:6379> debug object big:hash Value at:0x7fda95b0cb20 refcount:1 encoding:hashtable serializedlength:87777785 lru:9625559 lru_seconds_idle:2 (1.08s)
其中serializedlength表示key對應的value序列化以後的字節數,固然若是是字符串類型,徹底看能夠執行strlen,例如:
127.0.0.1:6379> strlen key (integer) 947394
這樣你就能夠用scan + debug object的方式遍歷Redis全部的鍵值,找到你須要閾值的數據了。
可是在使用debug object時候必定要注意如下幾點:
- debug object bigkey自己可能就會比較慢,它自己就會存在阻塞Redis的可能
- 建議在從節點執行
- 建議在節點本地執行
- 若是不關係具體字節數,徹底可使用scan + strlen|hlen|llen|scard|zcard替代,他們都是o(1)
3. memory usage
上面的debug object可能會比較危險、並且不太準確(序列化後的長度),有沒有更準確的呢?Redis 4.0開始提供memory usage命令能夠計算每一個鍵值的字節數(自身、以及相關指針開銷,具體的細節可查閱相關文章),例以下面是一次執行結果:
127.0.0.1:6379> memory usage big:hash (integer) 318663444
下面咱們來對比就能夠看出來,當前系統就一個key,總內存消耗是400MB左右,memory usage相比debug object仍是要精確一些的。
127.0.0.1:6379> dbsize (integer) 1 127.0.0.1:6379> hlen big:hash (integer) 5000000 #約300MB 127.0.0.1:6379> memory usage big:hash (integer) 318663444 #約85MB 127.0.0.1:6379> debug object big:hash Value at:0x7fda95b0cb20 refcount:1 encoding:hashtable serializedlength:87777785 lru:9625814 lru_seconds_idle:9 (1.06s) 127.0.0.1:6379> info memory # Memory used_memory_human:402.16M
若是你使用Redis 4.0+,你就能夠用scan + memory usage(pipeline)了,並且很好的一點是,memory不會執行很慢,固然依然是建議從節點 + 本地 。
4. 客戶端
上面三種方式都有一個問題,就是馬後炮,若是想很實時的找到bigkey,一方面你能夠試試修改Redis源碼,還有一種方式就是能夠修改客戶端,以jedis爲例,能夠在關鍵的出入口加上對應的檢測機制,例如以Jedis的獲取結果爲例子:
protected Object readProtocolWithCheckingBroken() { Object o = null; try { o = Protocol.read(inputStream); return o; }catch(JedisConnectionException exc) { UsefulDataCollector.collectException(exc, getHostPort(), System.currentTimeMillis()); broken = true; throw exc; }finally { if(o != null) { if(o instanceof byte[]) { byte[] bytes = (byte[]) o; if (bytes.length > threshold) { // 作不少事情,例如用ELK完成收集和展現 } } } } }
5. 監控報警
bigkey的大操做,一般會引發客戶端輸入或者輸出緩衝區的異常,Redis提供了info clients裏面包含的客戶端輸入緩衝區的字節數以及輸出緩衝區的隊列長度,能夠重點關注下:
若是想知道具體的客戶端,可使用client list命令來查找
redis-cli client list id=3 addr=127.0.0.1:58500 fd=8 name= age=3978 idle=25 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=26263554 events=r cmd=hgetall
6. 改源碼
這個其實也是能作的,可是各方面成本比較高,對於通常公司來講不適用。
建議的最佳實踐:
- Redis端與客戶端相結合:--bigkeys臨時用、scan長期作排除隱患(儘量本地化)、客戶端實時監控。
- 監控報警要跟上
- debug object儘可能少用
- 全部數據平臺化
- 要和開發同窗強調bigkey的危害
5、如何刪除
若是發現了bigkey,並且確認是垃圾是否是直接del就能夠了,來看一組數據:
能夠看到對於string類型,刪除速度仍是能夠接受的。但對於二級數據結構,隨着元素個數的增加以及每一個元素字節數的增大,刪除速度會愈來愈慢,存在阻塞Redis的隱患。因此在刪除它們時候建議採用漸進式的方式來完成:hscan、ltrim、sscan、zscan。
若是你使用Redis 4.0+,一條異步刪除unlink就解決,就能夠忽略下面內容。
1. 字符串
通常來講,對於string類型使用del命令不會產生阻塞。
del bigkey
2. hash
使用hscan命令,每次獲取部分(例如100個)field-value,在利用hdel刪除每一個field(爲了快速可使用pipeline)。
public void delBigHash(String bigKey) { Jedis jedis = new Jedis("127.0.0.1", 6379); // 遊標 String cursor = "0"; while(true) { ScanResult<Map.Entry<String, String>> scanResult = jedis.hscan(bigKey, cursor, new ScanParams().count(100)); // 每次掃描後獲取新的遊標 cursor = scanResult.getStringCursor(); // 獲取掃描結果 List<Entry<String, String>> list = scanResult.getResult(); if(list == null || list.size() == 0) { continue; } String[] fields = getFieldsFrom(list); // 刪除多個field jedis.hdel(bigKey, fields); // 遊標爲0時中止 if(cursor.equals("0")) { break; } } // 最終刪除key jedis.del(bigKey); } /** * 獲取field數組 */ private String[] getFieldsFrom(List<Entry<String, String>> list) { List<String> fields = new ArrayList<String>(); for (Entry<String, String> entry : list) { fields.add(entry.getKey()); } return fields.toArray(new String[fields.size()]); }
3. list
Redis並無提供lscan這樣的API來遍歷列表類型,可是提供了ltrim這樣的命令能夠漸進式的刪除列表元素,直到把列表刪除。
public void delBigList(String bigKey) { Jedis jedis = new Jedis("127.0.0.1", 6379); long llen = jedis.llen(bigKey); int counter = 0; int left = 100; while(counter < llen) { // 每次從左側截掉100個 jedis.ltrim(bigKey, left, llen); counter += left; } // 最終刪除key jedis.del(bigKey); }
4. set
使用sscan命令,每次獲取部分(例如100個)元素,在利用srem刪除每一個元素。
public void delBigSet(String bigKey) { Jedis jedis = new Jedis("127.0.0.1", 6379); // 遊標 String cursor = "0"; while(true) { ScanResult<String> scanResult = jedis.sscan(bigKey, cursor, new ScanParams().count(100)); // 每次掃描後獲取新的遊標 cursor = scanResult.getStringCursor(); // 獲取掃描結果 List<String> list = scanResult.getResult(); if(list == null || list.size() == 0) { continue; } jedis.srem(bigKey, list.toArray(new String[list.size()])); // 遊標爲0時中止 if(cursor.equals("0")) { break; } } // 最終刪除key jedis.del(bigKey);}
5. sorted set
使用zscan命令,每次獲取部分(例如100個)元素,在利用zremrangebyrank刪除元素。
public void delBigSortedSet(String bigKey) { long startTime = System.currentTimeMillis(); Jedis jedis = new Jedis(HOST, PORT); // 遊標 String cursor = "0"; while(true) { ScanResult<Tuple> scanResult = jedis.zscan(bigKey, cursor, new ScanParams().count(100)); // 每次掃描後獲取新的遊標 cursor = scanResult.getStringCursor(); // 獲取掃描結果 List<Tuple> list = scanResult.getResult(); if(list == null || list.size() == 0) { continue; } String[] members = getMembers(list); jedis.zrem(bigKey, members); // 遊標爲0時中止 if(cursor.equals("0")) { break; } } // 最終刪除key jedis.del(bigKey); } public void delBigSortedSet2(String bigKey) { Jedis jedis = new Jedis(HOST, PORT); long zcard = jedis.zcard(bigKey); int counter = 0; int incr = 100; while(counter < zcard) { jedis.zremrangeByRank(bigKey, 0, 100); // 每次從左側截掉100個 counter += incr; } // 最終刪除key jedis.del(bigKey); }
6、如何優化
1.拆分
big list: list一、list二、...listN
big hash:能夠作二次的hash,例如hash%100
日期類:key20190320、key2019032一、key_20190322。
2.本地緩存
減小訪問redis次數,下降危害,可是要注意這裏有可能所以本地的一些開銷(例如使用堆外內存會涉及序列化,bigkey對序列化的開銷也不小)
七、總結:
因爲開發人員對Redis的理解程度不一樣,在實際開發中出現bigkey在所不免,重要的能經過合理的檢測機制及時找到它們,進行處理。做爲開發人員應該在業務開發時不能將Redis簡單暴力的使用,應該在數據結構的選擇和設計上更加合理,例如出現了bigkey,要思考一下可不能夠作一些優化(例如二級索引)儘可能的讓這些bigkey消失在業務中,若是bigkey不可避免,也要思考一下要不要每次把全部元素都取出來(例若有時候僅僅須要hmget,而不是hgetall),刪除也是同樣,儘可能使用優雅的方式來處理。
因爲篇幅限制,更多的Redis介紹小編放在下面的文檔裏了,須要獲取完整文檔用以學習的朋友們能夠轉發+關注,私信領取,還有更多java源碼、筆記、資料哦!