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