(轉載)如何解決Redis大key問題,看這一篇就夠了!

Redis大key的一些場景及問題

大key場景

Redis使用者應該都遇到過大key相關的場景,比如:

1、熱門話題下評論、答案排序場景。

2、大V的粉絲列表。

3、使用不恰當,或者對業務預估不準確、不及時進行處理垃圾數據等。

大key問題

由于Redis主線程為單線程模型,大key也會帶來一些問題,如:

1、集群模式在slot分片均勻情況下,會出現數據和查詢傾斜情況,部分有大key的Redis節點占用內存多,QPS高。

2、大key相關的刪除或者自動過期時,會出現qps突降或者突升的情況,極端情況下,會造成主從復制異常,Redis服務阻塞無法響應請求。大key的體積與刪除耗時可參考下表:

key類型 field數量耗時

Hash100萬1000ms

List100萬1000ms

Set100萬1000ms

Sorted Set100萬1000ms

Redis 4.0之前的大key的發現與刪除方法

1、redis-rdb-tools工具。redis實例上執行bgsave,然后對dump出來的rdb文件進行分析,找到其中的大KEY。

2、redis-cli --bigkeys命令。可以找到某個實例5種數據類型(String、hash、list、set、zset)的最大key。

3、自定義的掃描腳本,以Python腳本居多,方法與redis-cli --bigkeys類似。

4、debug object key命令。可以查看某個key序列化后的長度,每次只能查找單個key的信息。官方不推薦。

redis-rdb-tools工具

關于rdb工具的詳細介紹請查看鏈接https://github.com/sripathikrishnan/redis-rdb-tools,在此只介紹內存相關的使用方法。基本的命令為 rdb -c memory dump.rdb (其中dump.rdb為Redis實例的rdb文件,可通過bgsave生成)。

輸出結果如下:

database,type,key,size_in_bytes,encoding,num_elements,len_largest_element

0,hash,hello1,1050,ziplist,86,22,

0,hash,hello2,2517,ziplist,222,8,

0,hash,hello3,2523,ziplist,156,12,

0,hash,hello4,62020,hashtable,776,32,

0,hash,hello5,71420,hashtable,1168,12,

可以看到輸出的信息包括數據類型,key、內存大小、編碼類型等。Rdb工具優點在于獲取的key信息詳細、可選參數多、支持定制化需求,結果信息可選擇json或csv格式,后續處理方便,其缺點是需要離線操作,獲取結果時間較長。

redis-cli --bigkeys命令

Redis-cli --bigkeys是redis-cli自帶的一個命令。它對整個redis進行掃描,尋找較大的key,并打印統計結果。

例如redis-cli -p 6379 --bigkeys

Scanning the entire keyspace to find biggest keys as well as

average sizes per key type. You can use -i 0.1 to sleep 0.1 sec

per 100 SCAN commands (not usually needed).

[00.72%] Biggest hash found so far 'hello6' with 43 fields

[02.81%] Biggest string found so far 'hello7' with 31 bytes

[05.15%] Biggest string found so far 'hello8' with 32 bytes

[26.94%] Biggest hash found so far 'hello9' with 1795 fields

[32.00%] Biggest hash found so far 'hello10' with 4671 fields

[35.55%] Biggest string found so far 'hello11' with 36 bytes

-------- summary -------

Sampled 293070 keys in the keyspace!

Total key length in bytes is 8731143 (avg len 29.79)

Biggest string found 'hello11' has 36 bytes

Biggest hash found 'hello10' has 4671 fields

238027 strings with 2300436 bytes (81.22% of keys, avg size 9.66)

0 lists with 0 items (00.00% of keys, avg size 0.00)

0 sets with 0 members (00.00% of keys, avg size 0.00)

55043 hashs with 289965 fields (18.78% of keys, avg size 5.27)

0 zsets with 0 members (00.00% of keys, avg size 0.00)

我們可以看到打印結果分為兩部分,掃描過程部分,只顯示了掃描到當前階段里最大的key。summary部分給出了每種數據結構中最大的Key以及統計信息。

redis-cli --bigkeys的優點是可以在線掃描,不阻塞服務;缺點是信息較少,內容不夠精確。掃描結果中只有string類型是以字節長度為衡量標準的。List、set、zset等都是以元素個數作為衡量標準,元素個數多不能說明占用內存就一定多。

自定義Python掃描腳本

通過strlen、hlen、scard等命令獲取字節大小或者元素個數,掃描結果比redis-cli --keys更精細,但是缺點和redis-cli --keys一樣,不贅述。

總之,之前的方法要么是用時較長離線解析,或者是不夠詳細的抽樣掃描,離理想的以內存為維度的在線掃描獲取詳細信息有一定距離。由于在redis4.0前,沒有lazy free機制;針對掃描出來的大key,DBA只能通過hscan、sscan、zscan方式漸進刪除若干個元素;但面對過期刪除鍵的場景,這種取巧的刪除就無能為力。我們只能祈禱自動清理過期key剛好在系統低峰時,降低對業務的影響。

Redis 4.0之后的大key的發現與刪除方法

Redis 4.0引入了memory usage命令和lazyfree機制,不管是對大key的發現,還是解決大key刪除或者過期造成的阻塞問題都有明顯的提升。

下面我們從源碼(摘自Redis 5.0.4版本)來理解memory usage和lazyfree的特點。

memory usage

{"memory",memoryCommand,-2,"rR",0,NULL,0,0,0,0,0}(server.c285?)void memoryCommand(client c) {/...//計算key大小是通過抽樣部分field來估算總大小。/elseif(!strcasecmp(c->argv[1]->ptr,"usage") &&c->argc >=3) { size_t usage = objectComputeSize(dictGetVal(de),samples);/...*/ }}(object.c1299?)

從上述源碼看到memory usage是通過調用objectComputeSize來計算key的大小。我們來看objectComputeSize函數的邏輯。

defineOBJ_COMPUTE_SIZE_DEF_SAMPLES 5 /* Default sample size. /size_tobjectComputeSize(robj o, size_t sample_size){/...代碼對數據類型進行了分類,此處只取hash類型說明//...//循環抽樣個field,累加獲取抽樣樣本內存值,默認抽樣樣本為5/while((de = dictNext(di)) != NULL && samples < sample_size) { ele = dictGetKey(de); ele2 = dictGetVal(de); elesize += sdsAllocSize(ele) + sdsAllocSize(ele2);elesize +=sizeof(structdictEntry); samples++; } dictReleaseIterator(di);/根據上一步計算的抽樣樣本內存值除以樣本量,再乘以總的filed個數計算總內存值/if(samples) asize += (double)elesize/samplesdictSize(d);/...*/ }(object.c779?)

由此,我們發現memory usage默認抽樣5個field來循環累加計算整個key的內存大小,樣本的數量決定了key的內存大小的準確性和計算成本,樣本越大,循環次數越多,計算結果更精確,性能消耗也越多。

我們可以通過Python腳本在集群低峰時掃描Redis,用較小的代價去獲取所有key的內存大小。以下為部分偽代碼,可根據實際情況設置大key閾值進行預警。

forkeyinr.scan_iter(count=1000):redis-cli ='/usr/bin/redis-cli'configcmd ='%s -h %s -p %s memory usage %s'% (redis-cli, rip,rport,key) keymemory = commands.getoutput(configcmd)

lazyfree機制

Lazyfree的原理是在刪除的時候只進行邏輯刪除,把key釋放操作放在bio(Background I/O)單獨的子線程處理中,減少刪除大key對redis主線程的阻塞,有效地避免因刪除大key帶來的性能問題。在此提一下bio線程,很多人把Redis通常理解為單線程內存數據庫, 其實不然。Redis將最主要的網絡收發和執行命令等操作都放在了主工作線程,然而除此之外還有幾個bio后臺線程,從源碼中可以看到有處理關閉文件和刷盤的后臺線程,以及Redis4.0新增加的lazyfree線程。

/* Background job opcodes /#defineBIO_LAZY_FREE 2/ Deferred objects freeing. */(bio.h38?)

下面我們以unlink命令為例,來理解lazyfree的實現原理。

{"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0},(server.c137?)void unlinkCommand(client *c) {delGenericCommand(c,1);}(db.c490?)

通過這幾段源碼可以看出del命令和unlink命令都是調用delGenericCommand,唯一的差別在于第二個參數不一樣。這個參數就是異步刪除參數。

/* This command implements DEL and LAZYDEL. /void delGenericCommand(client c, intlazy) {/.../int deleted =lazy? dbAsyncDelete(c->db,c->argv[j]) :dbSyncDelete(c->db,c->argv[j]);/.../}(db.c468?)

可以看到delGenericCommand函數根據lazy參數來決定是同步刪除還是異步刪除。當執行unlink命令時,傳入lazy參數值1,調用異步刪除函數dbAsyncDelete。否則執行del命令傳入參數值0,調用同步刪除函數dbSyncDelete。我們重點來看異步刪除dbAsyncDelete的實現邏輯:

defineLAZYFREE_THRESHOLD 64/定義后臺刪除的閾值,key的元素大于該閾值時才真正丟給后臺線程去刪除/intdbAsyncDelete(redisDb db, robj key){/...//lazyfreeGetFreeEffort來獲取val對象所包含的元素個數/size_tfree_effort = lazyfreeGetFreeEffort(val);/* 對刪除key進行判斷,滿足閾值條件時進行后臺刪除 /if(free_effort > LAZYFREE_THRESHOLD && val->refcount ==1) {atomicIncr(lazyfree_objects,1);bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);/將刪除對象放入BIO_LAZY_FREE后臺線程任務隊列/dictSetVal(db->dict,de,NULL);/將第一步獲取到的val值設置為null/ }/...*/}(lazyfree.c53?)

上面提到了當刪除key滿足閾值條件時,會將key放入BIO_LAZY_FREE后臺線程任務隊列。接下來我們來看BIO_LAZY_FREE后臺線程。

/.../elseif(type == BIO_LAZY_FREE) {if(job->arg1)/* 后臺刪除對象函數,調用decrRefCount減少key的引用計數,引用計數為0時會真正的釋放資源 / lazyfreeFreeObjectFromBioThread(job->arg1);elseif(job->arg2 && job->arg3)/ 后臺清空數據庫字典,調用dictRelease循環遍歷數據庫字典刪除所有key / lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);elseif(job->arg3)/ 后臺刪除key-slots映射表,在Redis集群模式下會用*/ lazyfreeFreeSlotsMapFromBioThread(job->arg3);}(bio.c197?)

unlink命令的邏輯可以總結為:執行unlink調用delGenericCommand函數傳入lazy參數值1,來調用異步刪除函數dbAsyncDelete,將滿足閾值的大key放入BIO_LAZY_FREE后臺線程任務隊列進行異步刪除。類似的后臺刪除命令還有flushdb async、flushall async。它們的原理都是獲取刪除標識進行判斷,然后調用異步刪除函數emptyDbAsnyc來清空數據庫。這些命令具體的實現邏輯可自行查看flushdbCommand部分源碼,在此不做贅述。

除了主動的大key刪除和數據庫清空操作外,過期key驅逐引發的刪除操作也會阻塞Redis服務。因此Redis4.0除了增加上述三個后臺刪除的命令外,還增加了4個后臺刪除配置項,分別為slave-lazy-flush、lazyfree-lazy-eviction、lazyfree-lazy-expire和lazyfree-lazy-server-del。

slave-lazy-flush:slave接收完RDB文件后清空數據選項。建議大家開啟slave-lazy-flush,這樣可減少slave節點flush操作時間,從而降低主從全量同步耗時的可能性。

lazyfree-lazy-eviction:內存用滿逐出選項。若開啟此選項可能導致淘汰key的內存釋放不夠及時,內存超用。

lazyfree-lazy-expire:過期key刪除選項。建議開啟。

lazyfree-lazy-server-del:內部刪除選項,比如rename命令將oldkey修改為一個已存在的newkey時,會先將newkey刪除掉。如果newkey是一個大key,可能會引起阻塞刪除。建議開啟。

上述四個后臺刪除相關的參數實現邏輯差異不大,都是通過參數選項進行判斷,從而選擇是否采用dbAsyncDelete或者emptyDbAsync進行異步刪除。

總結

在某些業務場景下,Redis大key的問題是難以避免的,但是,memory usage命令和lazyfree機制分別提供了內存維度的抽樣算法和異步刪除優化功能,這些特性有助于我們在實際業務中更好的預防大key的產生和解決大key造成的阻塞。關于Redis內核的優化思路也可從Redis作者Antirez的博客中窺測一二,他提出"Lazy Redis is better Redis"、"Slow commands threading"(允許在不同的線程中執行慢操作命令),異步化應該是Redis優化的主要方向。

Redis作為個推消息推送的一項重要的基礎服務,性能的好壞至關重要。個推將Redis版本從2.8升級到5.0后,有效地解決了部分大key刪除或過期造成的阻塞問題。未來,個推將會持續關注Redis 5.0及后續的Redis 6.0,與大家共同探討如何更好地使用Redis。

參考文檔:

1、http://antirez.com/news/93

2、http://antirez.com/news/126

作者:個推大數據
鏈接:http://www.lxweimin.com/p/50c0894c0a19
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容

  • NOSQL類型簡介鍵值對:會使用到一個哈希表,表中有一個特定的鍵和一個指針指向特定的數據,如redis,volde...
    MicoCube閱讀 4,041評論 2 27
  • 1.1 資料 ,最好的入門小冊子,可以先于一切文檔之前看,免費。 作者Antirez的博客,Antirez維護的R...
    JefferyLcm閱讀 17,090評論 1 51
  • 企業級redis集群架構的特點 海量數據 高并發 高可用 要達到高可用,持久化是不可減少的,持久化主要是做災難恢復...
    lucode閱讀 2,219評論 0 7
  • 1 Redis介紹1.1 什么是NoSql為了解決高并發、高可擴展、高可用、大數據存儲問題而產生的數據庫解決方...
    克魯德李閱讀 5,334評論 0 36
  • 記得三、四年前,曾經一度我十分害怕當眾說話。記得經常去參加一個組織學習,主持人要求每個人要有分享。為了害怕分享。我...
    茶話心語閱讀 428評論 0 0