深入理解Redis大key的危害及解決方案
一、背景
Redis作為后端開發(fā)中的一個常用組件,在開發(fā)過程中承擔(dān)著非常重要的作用。在其實際使用過程中,我們常常會面臨一些技術(shù)挑戰(zhàn),其中常見的問題就包括大key問題。當(dāng)某些數(shù)據(jù)量較大的鍵值對(如富文本等)存儲在Redis中時,這些大key的讀寫操作會明顯影響系統(tǒng)的性能,嚴(yán)重時會導(dǎo)致系統(tǒng)出現(xiàn)卡頓的問題。因此,Redis大key的監(jiān)控與治理是互聯(lián)網(wǎng)開發(fā)中使用Redis時必須面對的一個課題,本文將按照redis大key的定義與評判標(biāo)準(zhǔn)、危害與解決方式的順序來做個總結(jié)。
二、什么是大key
通常來說,Big Key指的就是某個key對應(yīng)的value很大,占用的Redis空間很大,是value過大的問題。這是很多文章中會提到的一點, 除此之外,這里想強(qiáng)調(diào)的是Redis的key的大小同樣也會對性能存在隱患 ,而為什么我們會比較少去討論這一點呢,那是因為key是我們?nèi)藶橹苯釉O(shè)置的,其相對來說更可控一點,但是它也同樣不可忽視。其實我們從存儲結(jié)構(gòu)就不難分析得到上述結(jié)論:
根結(jié)構(gòu)為RedisServer,其中包含RedisDB(數(shù)據(jù)庫)。而RedisDB實際上是使用Dict(字典)結(jié)構(gòu)對Redis中的kv進(jìn)行存儲的。這里的key即字符串,value可以是string/hash/list/set/zset這五種對象之一。
Dict字典結(jié)構(gòu)中,存儲數(shù)據(jù)的主題為DictHt,即哈希表。而哈希表本質(zhì)上是一個DictEntry(哈希表節(jié)點)的數(shù)組,并且使用鏈表法解決哈希沖突問題(關(guān)于哈希沖突的解決方法可以參考大佬的文章 解決哈希沖突的常用方法分析)。
所以在實際存儲時,key和value都是存儲在DictEntry中的。所以基本上來說,大key和大value帶來的內(nèi)存不均和網(wǎng)絡(luò)IO壓力都是一致的,只是key相較于value還多一個做hashcode和比較的過程(鏈表中進(jìn)行遍歷比較key),會有更多的內(nèi)存相關(guān)開銷。
通過redis的key和value存儲來看,其大小帶來的影響基本一致,都會給系統(tǒng)性能帶來不小的影響。
三、大key評價標(biāo)準(zhǔn)
一般認(rèn)為string類型控制在10KB以內(nèi),hash、list、set、zset元素個數(shù)不要超過10000個。
但在實際業(yè)務(wù)中,大Key的判定仍然需要根據(jù)Redis的實際使用場景、業(yè)務(wù)場景來進(jìn)行綜合判斷,通常都會以數(shù)據(jù)大小與成員數(shù)量來判定。在不同互聯(lián)網(wǎng)公司中,其對Redis大Key的定義標(biāo)準(zhǔn)也略有差異,例如已知業(yè)內(nèi)公司的評價標(biāo)準(zhǔn)如下:
- 阿里標(biāo)準(zhǔn):字符推薦小于10KB,但是他們內(nèi)網(wǎng)建議是小于1KB,集合個數(shù)推薦低于1000;
- 美團(tuán)標(biāo)準(zhǔn):字符推薦不能超過512KB,集合個數(shù)不能超過1W個,單個不能超于1M;
- 攜程標(biāo)準(zhǔn):字符類型大小控制在10KB以內(nèi),hash/list/set/zset等包含元素個數(shù)建議控制在1000以內(nèi),單個集合大小沒有限制。
四、大key 產(chǎn)生的原因與場景
大 key 通常是由于下面這些原因產(chǎn)生的:
redis數(shù)據(jù)結(jié)構(gòu)使用不恰當(dāng)
- 將Redis用在并不適合其能力的場景,造成key的value過大,如使用String類型的key存放大體積二進(jìn)制文件型數(shù)據(jù)(富文本類型數(shù)據(jù))
未及時清理垃圾數(shù)據(jù)
- 沒有對無效數(shù)據(jù)進(jìn)行定期清理,造成如Hash類型key中的成員持續(xù)不斷的增加
對業(yè)務(wù)預(yù)估不準(zhǔn)確
- 業(yè)務(wù)上線前規(guī)劃設(shè)計考慮不足沒有對key中的成員進(jìn)行合理的拆分,造成個別key中的成員數(shù)量過多
明星、網(wǎng)紅的粉絲列表、某條熱點新聞的評論列表
- 用List數(shù)據(jù)結(jié)構(gòu)保存熱點新聞的評論列表,因為粉絲數(shù)量巨大,熱點新聞因為點擊率、評論數(shù)會很多,這樣List集合中存放的元素就會很多,可能導(dǎo)致value過大,進(jìn)而產(chǎn)生Big Key問題。
引發(fā)大Key問題的幾個常見場景如下:
隊列型應(yīng)用
- 將Redis用作隊列處理任務(wù),如果隊列消費速度跟不上生產(chǎn)速度,隊列會越來越大,最終形成"大key"
統(tǒng)計型應(yīng)用
- 按天存儲某項功能或網(wǎng)站的用戶集合信息,如果用戶量較大,這類"統(tǒng)計型"數(shù)據(jù)容易形成"大key"
緩存型應(yīng)用
- 業(yè)務(wù)上線前規(guī)劃設(shè)計考慮不足沒有對key中的成員進(jìn)行合理的拆分,造成個別Key中的成員數(shù)量過多
五、大key影響與危害
性能下降
- 大key的讀寫操作可能會消耗更多的CPU和內(nèi)存資源,導(dǎo)致redis的響應(yīng)時間變慢
內(nèi)存使用不均衡
- 在集群環(huán)境中,大key可能導(dǎo)致某些數(shù)據(jù)分片的內(nèi)存使用率遠(yuǎn)高于其他分片,造成內(nèi)存資源分配不均勻
帶寬占用
- 如果大key被頻繁訪問,可能會占用大量的網(wǎng)絡(luò)帶寬,影響其他服務(wù)的性能
阻塞問題
- 由于redis是單線程的,操作大key可能會造成redis服務(wù)阻塞,尤其是在執(zhí)行清除操作或者過期時間到達(dá)時
內(nèi)存溢出
- 如果redis實例的內(nèi)存達(dá)到maxmemory參數(shù)定義的上限,大key的存在可能導(dǎo)致操作阻塞或者重要key被逐出,甚至引發(fā)內(nèi)存溢出
緩存穿透
- 大key的頻繁清理可能導(dǎo)致緩存穿透問題,影響緩存效率
數(shù)據(jù)傾斜
- 在集群架構(gòu)下,大key可能導(dǎo)致訪問傾斜,即扣個數(shù)據(jù)分片被大量訪問,而其他分片處于空閑狀態(tài),可能會引起鏈接數(shù)耗盡問題
緩存擊穿
- 熱key的請求壓力超出redis的承受能力,可能導(dǎo)致緩存擊穿,大量請求直接指向后端存儲層,造成存儲層訪問量激增
六、大key檢查與發(fā)現(xiàn)
6.1 使用 --bigkeys參數(shù)
–bigkeys 是 redis 自帶的命令,對整個 Key 進(jìn)行掃描,統(tǒng)計 string,list,set,zset,hash 這幾個常見數(shù)據(jù)類型中每種類型里的最大的 key。String 類型統(tǒng)計的是 value 的字節(jié)數(shù),另外 4 種復(fù)雜結(jié)構(gòu)的類型統(tǒng)計的是元素個數(shù),不能直觀的看出 value 占用字節(jié)數(shù),所以 --bigkeys 對分析 string 類型的大 key 是有用的,而復(fù)雜結(jié)構(gòu)的類型還需要一些第三方工具(注:元素個數(shù)少,不一定 value 不大;元素個數(shù)多,也不一定 value 就大)。
–bigkeys 是以 scan 延遲計算的方式掃描所有 key,因此執(zhí)行過程中不會阻塞 redis,但實例存在大量的 keys 時,命令執(zhí)行的時間會很長,這種情況建議在 slave 上掃描。
–bigkeys 其實就是找出類型中最大的 key,最大的 key 不一定是大 key,最大的 key 都不超過 10kb 的話,說明不存在大 key。但某種類型如果存在較多的大key (>10kb),只會統(tǒng)計 top1 的那個 key,如果要統(tǒng)計所有大于 10kb 的 key,需要用第三方工具掃描 rdb 持久化文件。
redis-cli -h 127.0.0.1 -p 6379 -a "password" --bigkeys
6.2 使用scan命令
使用 Redis 自帶的 scan命令,這個命令會以遍歷的方式分析 Redis 實例中的所有 key,并返回整體統(tǒng)計信息,結(jié)合其他命令(如List 類型:LLEN
命令;Hash 類型:HLEN
命令;Set 類型:SCARD
命令;Sorted Set 類型:ZCARD
命令)我們可以識別出大Key。scan命令的優(yōu)勢在于它可以在不阻塞Redis實例的情況下進(jìn)行遍歷。
redis-cli --scan --pattern '*' --count 1000
除此之外,其實也可以通過腳本等定期主動掃描Redis鍵值,當(dāng)鍵值超過閾值則記錄為BigKey。
import redis import time # Redis連接 redis_client = redis.Redis(host='localhost', port=6379) # 大鍵閾值 - 100MB BIG_KEY_THRESHOLD = 100 * 1024 * 1024 # 掃描間隔 - 1小時 SCAN_INTERVAL = 3600 def scan_big_keys(): cursor = '0' big_keys = [] while cursor != 0: cursor, keys = redis_client.scan(cursor=cursor, count=100) for key in keys: size = redis_client.debug_object(key)['serializedlength'] if size > BIG_KEY_THRESHOLD: big_keys.append({'key': key, 'size': size}) return big_keys while True: big_keys = scan_big_keys() if big_keys: print(f'Found {len(big_keys)} big keys') for bk in big_keys: print(f' {bk["key"]} {bk["size"]}') time.sleep(SCAN_INTERVAL)
主要步驟包括:
? 1.使用SCAN命令漸進(jìn)式掃描鍵空間
? 2.調(diào)用DEBUG OBJECT命令獲取鍵值大小
? 3.比較大小判斷是否為大鍵
? 4.定期循環(huán)掃描這個腳本可以靈活調(diào)整大鍵閾值、掃描間隔等參數(shù)。
可以將它部署為持續(xù)運行的任務(wù),自動發(fā)現(xiàn)Redis中的大鍵,但是這種方式結(jié)合了DEBUG OBJECT命令,其運行代價較大,在其運行時,進(jìn)入Redis的其余請求將會被阻塞直到其執(zhí)行完畢。因此,如果使用這種方式進(jìn)行檢測,最好讓其在對用戶體驗影響最小的時候運行(如凌晨兩三點)。
6.3 使用 memory 命令查看 key 的大小
在Redis4.0之前,只能通過DEBUG OBJECT命令估算key的內(nèi)存使用(字段serializedlength),但DEBUG OBJECT命令是存在誤差的。Redis4.0以后的版本中可以使用 memory 命令查看 key 的大小。如果當(dāng)前key存在,則返回key的value實際使用內(nèi)存估算值,如果key不存在,則返回nil。
redis-cli -h 127.0.0.1 -p 6379 -a password MEMORY USAGE keyname1 (integer) 157481 MEMORY USAGE keyname2 (integer) 312583 MEMORY USAGE keyname3 (nil)
6.4 使用 Rdbtools 工具包
Rdbtools 是 Python寫的 一個第三方開源工具,用來解析 Redis 快照文件,除了解析 rdb 文件,其還提供了統(tǒng)計單個 key 大小的工具。使用Rdbtools 離線分析工具來掃描RDB持久化文件,優(yōu)點在于無性能損耗、獲取的key信息詳細(xì)、可選參數(shù)多、支持定制化需求,結(jié)果信息可選擇json或csv格式,后續(xù)處理方便,但其缺點是需要離線操作,獲取結(jié)果時間較長。
①安裝
git clone https://github.com/sripathikrishnan/redis-rdb-tools cd redis-rdb-tools sudo && python setup.py install
②使用從 dump.rdb 快照文件統(tǒng)計, 將所有 > 10kb 的 key 輸出到一個 csv 文件
rdb dump.rdb -c memory --bytes 10240 -f live_redis.csv
6.5 代碼埋點
如果是代碼寫入Redis,例如在Java代碼中將數(shù)據(jù)寫入到Redis里,可以直接在代碼里進(jìn)行埋點統(tǒng)計。
6.6 公有云的Redis分析服務(wù)
基于某些公有云或者公司內(nèi)部架構(gòu)的Redis一般都會有可視化的頁面和分析工具,來幫助我們定位大key,當(dāng)然頁面底層也可能是基于bigkeys或者rdb文件離線分析的結(jié)果。如阿里云社區(qū)提供了一款基于Python編寫的大key定位工具:https://developer.aliyun.com/article/117042
七、大key解決方式
實際上解決大key問題的核心就是減小value的占用空間,大多數(shù)方法都集中在對存量的大key進(jìn)行改造,如清理過期數(shù)據(jù)等方式,除此之外,也有通用的方案,在開始存儲時就預(yù)先設(shè)計好,如壓縮數(shù)據(jù)及拆分存儲等方式。
7.1 增加監(jiān)控告警
通過監(jiān)控系統(tǒng)并設(shè)置合理的Redis內(nèi)存報警閾值來提醒我們此時可能有大Key正在產(chǎn)生,如:Redis內(nèi)存使用率超過70%,Redis內(nèi)存1小時內(nèi)增長率超過20%等。如果是應(yīng)用代碼里寫入數(shù)據(jù)到Redis,那么做好埋點,然后加監(jiān)控告警也是非常重要的,既可以對歷史數(shù)據(jù)進(jìn)行監(jiān)控,定位大key,又可以避免新增的邏輯代碼中出現(xiàn)大key這一隱患。因此,埋點監(jiān)控是基石。
7.2 數(shù)據(jù)結(jié)構(gòu)優(yōu)化
優(yōu)化 redis 的數(shù)據(jù)結(jié)構(gòu),使用合適的數(shù)據(jù)結(jié)構(gòu)來存儲數(shù)據(jù),避免出現(xiàn) redis 大 key 的情況。例如,文件二進(jìn)制數(shù)據(jù)不使用 String 保存、使用 HyperLogLog 統(tǒng)計頁面 UV、Bitmap 保存狀態(tài)信息。
7.3 清理過期數(shù)據(jù)
如果某個Key有業(yè)務(wù)不斷以增量方式寫入大量的數(shù)據(jù),并且忽略了其時效性,這樣會導(dǎo)致大量的失效數(shù)據(jù)堆積??梢酝ㄟ^定時任務(wù)的方式,對失效數(shù)據(jù)進(jìn)行清理。設(shè)置合理的過期時間。為每個key設(shè)置過期時間,并設(shè)置合理的過期時間,以便在數(shù)據(jù)失效后自動清理,避免長時間累積的大Key問題。
- Redis 4.0及之后版本:可以通過UNLINK命令安全地刪除大Key甚至特大Key,該命令能夠以非阻塞的方式,逐步地清理傳入的Key。 Redis UNLINK 命令類似與 DEL 命令,表示刪除指定的 key,如果指定 key 不存在,命令則忽略。 UNLINK 命令不同與 DEL
命令在于它是異步執(zhí)行的,因此它不會阻塞。 UNLINK 命令是非阻塞刪除,非阻塞刪除簡言之,就是將刪除操作放到另外一個線程去處理。 - Redis 4.0之前的版本:建議先通過SCAN命令讀取部分?jǐn)?shù)據(jù),然后進(jìn)行刪除,避免一次性刪除大量key導(dǎo)致Redis阻塞。 Redis Scan 命令用于迭代數(shù)據(jù)庫中的數(shù)據(jù)庫鍵。 SCAN 命令是一個基于游標(biāo)的迭代器,每次被調(diào)用之后, 都會向用戶返回一個新的游標(biāo),
用戶在下次迭代時需要使用這個新游標(biāo)作為 SCAN 命令的游標(biāo)參數(shù), 以此來延續(xù)之前的迭代過程。
啟用內(nèi)存淘汰策略。啟用Redis的內(nèi)存淘汰策略,例如LRU(Least Recently Used,最近最少使用),以便在內(nèi)存不足時自動淘汰最近最少使用的數(shù)據(jù),防止大Key長時間占用內(nèi)存。
7.4 壓縮數(shù)據(jù)
在向Redis中寫數(shù)據(jù)時,可以使用序列化、壓縮算法將key的大小控制在合理范圍內(nèi),但是需要注意序列化、反序列化都會帶來一定的消耗。
Redis 支持多種壓縮算法,例如 LZF、QUICKLZ 和 GZIP 等算法。不過經(jīng)過推薦和測試,本文推薦ZSTD為壓縮工具,因為相比其他主流壓縮算法ZSTD提供了更優(yōu)秀的壓縮比。另外在使用隨機(jī)字符串本地循環(huán)測試后,發(fā)現(xiàn)處理較小規(guī)模的隨機(jī)字符串時,ZSTD壓縮效果并不明顯,如下表格所示,對于0.1KB的數(shù)據(jù)壓縮后的文件大小甚至?xí)兴黾印_@可能是由于小數(shù)據(jù)量的特點導(dǎo)致壓縮算法無法充分發(fā)揮其優(yōu)勢,也有可能是該壓縮算法不適用于小數(shù)據(jù)量壓縮。因此,為了避免浪費無謂壓縮帶來的CPU損耗和耗時,可以引入閾值判斷機(jī)制,只有當(dāng)數(shù)據(jù)大小超過10KB時才觸發(fā)ZSTD壓縮操作。
壓縮前(KB) | 壓縮后(KB) | 壓縮率 |
---|---|---|
0.01 | 0.022 | -89% |
0.1 | 0.122 | -9% |
1 | 0.784 | 21% |
10 | 7.47 | 25% |
100 | 74.6 | 25% |
1000 | 746 | 25% |
7.5 拆分存儲
壓縮技術(shù)確實可以有效減小value的占用空間,從而解決大部分key問題,但是如果壓縮后,value還是很大,那么可以進(jìn)一步對key進(jìn)行拆分。key拆分的具體思路是:將原先的單個大key拆分成一個主key和多個子key,其中主key主要保存子key之間的關(guān)聯(lián)信息,但不包含具體的值,而子key負(fù)責(zé)存儲拆分過后的值信息。通過拆分大幅降低單個key的value大小,從而有效解決原有的大key問題。
往Redis中寫的拆分方案如下圖所示:
具體步驟:
首先,假設(shè)有個key 為key:user,然后需要判斷key:user對應(yīng)的值的size是否超過指定閾值,例如10KB,如果size小于10KB,則之間將其值寫入到Redis中;如果size小大于10KB,則需要將其值按照一定方式進(jìn)行分解,生成一個key和多個子key。在拆分完key之后,就需要保證主key和子key列表能夠批量原子寫入到Redis中,這一操作非常重要,如果不能保證其原子性就可能會出現(xiàn)部分key寫入失敗的情況,進(jìn)而導(dǎo)致并發(fā)讀取數(shù)據(jù)的不一致問題。
從Redis中讀取的操作其實就是寫入的逆操作,其主要就是將生成的主key和子key重新合并起來,方案如下圖所示:
具體步驟:
首先,根據(jù)key:user從Redis中獲取到子key列表,根據(jù)子key列表再利用mget命令從Redis中獲取子key的值,然后按一定方式進(jìn)行合并還原,最終校驗length。
7.5.1 按時間/業(yè)務(wù)拆分
如果 Big Key 包含的是按時間順序排列的數(shù)據(jù),可以考慮按時間范圍拆分一下,但是拆分后的每個key對應(yīng)的值應(yīng)該進(jìn)行二次校驗,確認(rèn)其大小是否低于大key的閾值判斷標(biāo)準(zhǔn)。
7.5.2 按哈希拆分
如果要存儲的數(shù)據(jù)不是按時間順序排列的數(shù)據(jù),那么可以考慮哈希的方式進(jìn)行拆分。而這一步首先我們需要先確定拆分的子key的數(shù)量:可以通過size(value) / 10KB計算出子key數(shù)量,然后每次存取的時候,先在本地計算field的hash值,模除 10000, 就確定了該field落在哪個key上。
八、總結(jié)
大key和大value的危害是一致的:內(nèi)存不均、阻塞請求、阻塞網(wǎng)絡(luò)等。本文主要詳細(xì)介紹了大value產(chǎn)生的原因、影響、檢測方法和解決方案。通過優(yōu)化數(shù)據(jù)結(jié)構(gòu)設(shè)計、壓縮存儲、拆分存儲等方法,我們可以有效地解決和預(yù)防大key問題,從而提高Redis系統(tǒng)的穩(wěn)定性和性能。
到此這篇關(guān)于深入理解Redis大key的危害及解決方案的文章就介紹到這了,更多相關(guān)Redis大key內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于 Redis 的 JWT令牌失效處理方案(實現(xiàn)步驟)
當(dāng)用戶登錄狀態(tài)到登出狀態(tài)時,對應(yīng)的JWT的令牌需要設(shè)置為失效狀態(tài),這時可以使用基于Redis 的黑名單方案來實現(xiàn)JWT令牌失效,本文給大家分享基于 Redis 的 JWT令牌失效處理方案,感興趣的朋友一起看看吧2024-03-03redis的key出現(xiàn)的\xac\xed\x00\x05t\x00亂碼問題及解決
這篇文章主要介紹了redis的key出現(xiàn)的\xac\xed\x00\x05t\x00亂碼問題及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09