redis之bigkey(看這一篇就夠)


bigkey

1、bigkey帶來的問題

  1. 如果是集群模式下,無法做到負載均衡,導致請求傾斜到某個實例上,而這個實例的QPS會比較大,內存占用也較多;對於Redis單線程模型又容易出現CPU瓶頸,當內存出現瓶頸時,只能進行縱向庫容,使用更牛逼的服務器。
  2. 涉及到大key的操作,尤其是使用hgetall、lrange 0 -1、get、hmget 等操作時,網卡可能會成為瓶頸,也會到導致堵塞其它操作,qps 就有可能出現突降或者突升的情況,趨勢上看起來十分不平滑,嚴重時會導致應用程序連不上,實例或者集群在某些時間段內不可用的狀態。
  3. 假如這個key需要進行刪除操作,如果直接進行DEL 操作,被操作的實例會被Block住,導致無法響應應用的請求,而這個Block的時間會隨着key的變大而變長。

2、bigkey是如何產生的

一般來說,bigkey是由於程序員的程序設計不當,或對數據規模預料不清楚造成的:
1、社交類:粉絲列表,如果某些明顯或大V,一定是bigkey
2、統計類:如果按天存儲某項功能或網站的用戶集合,除非沒幾個人用,否則必定是bigkey
3、緩存類:作為數據庫數據的冗余存儲,這種是redis的最常用場景,但有2點要注意:
1)是不是有必要把所有數據都緩存
2)有沒有相關關聯的數據
舉個例子,該同學把某明星一個專輯下的所有視頻信息都緩存成了一個巨大的json,這個json達到了6MB。

3、查找bigKey的方法

  1. 在redis實例上執行bgsave,然后我們對dump出來的rdb文件進行分析,找到其中的大KEY
  2. 有個不太推薦的命令,debug object xxx 可以看到這個key在內存中序列化后的大小,當然我們可以通過SCAN+debug object xxx 得到當前實例所有key的大小。
  3. redis-cli 原生自帶 –bigkeys 功能,可以找到某個實例 5種數據類型(String、hash、list、set、zset)的最大key。

4、直接刪除bigkey的風險

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類型 Item數量 耗時
Hash ~100萬 ~1000ms
List ~100萬 ~1000ms
Set ~100萬 ~1000ms
Sorted Set ~100萬 ~1000ms

5、如何優雅地刪除各類大Key

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

先給鍵改名。

5.1 Delete Large Hash Key

通過hscan命令,每次獲取500個字段,再用hdel命令,每次刪除1個字段。Python代碼:

def del_large_hash():
  r = redis.StrictRedis(host='redis-host1', port=6379)
    large_hash_key ="xxx" 
    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])

5.2 Delete Large Set Key

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

def del_large_set():
  r = redis.StrictRedis(host='redis-host1', port=6379)
  large_set_key = 'xxx'   
  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)

5.3 Delete Large List Key

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

def del_large_list():
  r = redis.StrictRedis(host='redis-host1', port=6379)
  large_list_key = 'xxx'  
  while r.llen(large_list_key)>0:
      r.ltrim(large_list_key, 0, -101) 

5.4 Delete Large Sorted set key

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

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)

5.5 后台刪除之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 //異步清理實例數據

注意:DEL命令,還是阻塞的刪除操作。

FLUSHALL/FLUSHDB ASYNC

通過對FLUSHALL/FLUSHDB添加ASYNC異步清理選項,redis在清理整個實例或DB時,操作都是異步的。

127.0.0.1:7000> DBSIZE
(integer) 1812295
127.0.0.1:7000> flushall //同步清理實例數據,180萬個key耗時1020毫秒
OK
(1.02s)
127.0.0.1:7000> DBSIZE
(integer) 1812637
127.0.0.1:7000> flushall async //異步清理實例數據,180萬個key耗時約9毫秒
OK
127.0.0.1:7000> SLOWLOG get
 1) 1) (integer) 2996109
 2) (integer) 1505465989
 3) (integer) 9274 //指令運行耗時9.2毫秒
 4) 1) "flushall" 
 2) "async"
 5) "127.0.0.1:20110"
 6) ""

被動刪除

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來執行。

6、鍵值設計

key名設計

可讀性和可管理性(建議)

以業務名(或數據庫名)為前綴(防止key沖突),用冒號分隔,比如業務名:表名:id

set trade:order:1//業務名:表名:id

簡潔性(建議)

保證語義的情況下,減低key長度,key過長也占用內存空間

user:{uid}:friends:messages:{mid} 簡化為 u:{uid}:fr:m:{mid}

不要包含特殊字符(強制)

反例:包含空格、換行、單雙引號以及其他轉義字符

value設計

拒絕bigkey

在Redis中,一個字符串最大512MB,一個二級數據結構(例如hash、list、set、zset)可以存儲大約40億個(2^32-1)個元素,但實際中如果下面兩種情況,我就會認為它是bigkey。

1.字符串類型:

它的big體現在單個value值很大,一般認為超過10KB就是bigkey。

2.非字符串類型:(hash,list,set,zset等)

哈希、列表、集合、有序集合,它們的big體現在元素個數太多。
一般來說hash、list、set、zset元素個數不要超過5000。
反例:一個包含200萬個元素的list。

3.bigkey的刪除

非字符串的bigkey,不要使用del刪除,使用hscan、sscan、zscan方式漸進式刪除,同時要注意防止bigkey過期時間自動刪除問題(例如一個200萬的zset設置1小時過期,會觸發del操作,如果沒有使用Redis 4.0的過期異步刪除(lazyfree-lazy-expire yes),就會存在阻塞Redis的可能性)

7、優化bigkey

優化bigkey

1.一個字拆,大拆小

hash結構 比如一個big hash中有100萬的數據可以通過key的名稱做定義將100萬的數據進行拆分成200個key,每個key中存放5000個數據
list結構也是同樣操作,一個list的key存放5000個集合,拆開來存

2.避開危險操作

如果必須使用bigkey的話,那操作的時候避開hgetall、lrange、smembers、zrange、sinter等全數據查詢的命令,有遍歷的需求可以使用hscan、sscan、zscan代替(例如有時候僅僅需要hmget,而不是hgetall),刪除也是一樣,盡量使用優雅的方式來處理。

3.合理使用數據類型(推薦)

例如:實體類型(要合理控制和使用數據結構,但也要注意節省內存和性能之間的平衡)
正例:

hmset user:1 name tom age 19 favor football

反例:

set user:1:name tom
set user:1:age 19
set user:1:favor football

4.控制key的生命周期,redis不是垃圾桶(推薦)

建議使用expire設置過期時間(條件允許可以打散過期時間,防止集中過期)。

命令使用

1.O(N)命令關注N的數量

例如hgetall、lrange、smembers、zrange、sinter等並非不能使用,但是需要明確N的值。有遍歷的需求可以使用hscan、sscan、zscan代替。

2.禁用命令

禁止線上使用keys、flushall、flushdb等,通過redis的rename機制禁掉命令,或者使用scan的方式漸進式處理。

3.合理使用select

redis自帶的多數據庫較弱,使用數字進行區分,很多客戶端支持較差,同時多業務用多數據庫實際還是單線程處理,會有干擾。

4.使用批量操作提高效率

原生命令:例如mget、mset。
非原生命令:可以使用pipeline提高效率。

但要注意控制一次批量操作的元素個數(例如500以內,實際也和元素字節數有關)。
注意兩者不同:

原生命令是原子操作,pipeline是非原子操作。
pipeline可以打包不同的命令,原生命令做不到
pipeline需要客戶端和服務端同時支持。

5.Redis事務功能較弱,不建議過多使用,可以用lua替代


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM