一文了解發(fā)現(xiàn)并解決Redis熱key與大key問題
使用緩存
為什么使用緩存
緩存就是數(shù)據(jù)交換的緩沖區(qū)(稱作Cache),目的就是提高我們的接口性能,特別是那些需要大量CPU計(jì)算和I/O獲取的數(shù)據(jù)。
使用緩存帶來的問題
緩存雖然能夠提高應(yīng)用程序的性能,但也會(huì)帶來一些問題。比如:緩存失效,緩存擊穿,緩存雪崩,數(shù)據(jù)一致性問題
緩存雪崩
緩存失效為什么會(huì)帶來問題呢?試想一下,單個(gè)的緩存失效其實(shí)并不會(huì)引發(fā)多大的問題,問題在于當(dāng)大量的Key同時(shí)失效時(shí),在高并發(fā)的情況下,大量的請(qǐng)求同時(shí)到數(shù)據(jù)庫層,會(huì)給數(shù)據(jù)庫層帶來壓力,從而引發(fā)其他的問題。
解決方案
優(yōu)化過期時(shí)間
既然是同時(shí)失效,那么我們只需要在Key的失效時(shí)間上再加上一個(gè)隨機(jī)時(shí)間就好了,也就是失效時(shí)間 + 隨機(jī)時(shí)間。go-zero 上已經(jīng)有相關(guān)的代碼,我簡單摘抄出來看下
// A Unstable is used to generate random value around the mean value base on given deviation. type Unstable struct { deviation float64 r *rand.Rand lock *sync.Mutex } // AroundDuration returns a random duration with given base and deviation. func (u Unstable) AroundDuration(base time.Duration) time.Duration { u.lock.Lock() val := time.Duration((1 + u.deviation - 2*u.deviation*u.r.Float64()) * float64(base)) u.lock.Unlock() return val }
優(yōu)化緩存
采用多級(jí)緩存,不同級(jí)別緩存設(shè)置的超時(shí)時(shí)間不同,及時(shí)某個(gè)級(jí)別緩存都過期,也有其他級(jí)別緩存兜底。代碼如下,完整代碼見:cache_redis.go
func (r *RedisCacheClient) Get(ctx context.Context, key string, fetch fetchFunc) (result []byte, err error) { var byteValue []byte fullKey := getFullKey(r.prefix, key) fullKeyByte, _ := json.Marshal(fullKey) if val, err := r.localCache.Get(fullKeyByte); err == nil { r.status.IncrementLocalCacheHit() return val, nil } r.status.IncrementLocalCacheMiss() startTime := time.Now() byteValue, err = r.client.Get(fullKey).Bytes() elapsed := time.Since(startTime).Milliseconds() for _, p := range r.plugins { p.OnGetRequestEnd(ctx, cmdGet, elapsed, fullKey, err) } // 數(shù)據(jù)源拉取原始數(shù)據(jù) ........ }
緩存擊穿
對(duì)于某些key設(shè)置了過期時(shí)間,但是其是熱點(diǎn)數(shù)據(jù),如果某個(gè)key失效,可能大量的請(qǐng)求打過來,緩存未命中,然后去數(shù)據(jù)庫訪問,此時(shí)數(shù)據(jù)庫訪問量會(huì)急劇增加。
解決方案
多級(jí)緩存+singleflight
我們可以設(shè)置多級(jí)緩存,每一級(jí)緩存失效時(shí)間不一樣,某個(gè)級(jí)別緩存過期,也有其他級(jí)別緩存兜底。而且再加上singleflight 限制,就可以做每一個(gè)服務(wù)實(shí)例只有一個(gè)請(qǐng)求最終到數(shù)據(jù)庫源上,大大減輕了數(shù)據(jù)源壓力
緩存穿透
緩存穿透是指查詢的數(shù)據(jù)在數(shù)據(jù)庫是沒有的,那么在緩存中自然也沒有,所以,在緩存中查不到就會(huì)去數(shù)據(jù)庫取查詢,這樣的請(qǐng)求一多,那么我們的數(shù)據(jù)庫的壓力自然會(huì)增大。
解決方案
設(shè)置Null值
- 約定:對(duì)于返回為Null的依然緩存,對(duì)于拋出異常的返回不進(jìn)行緩存,注意不要把拋異常的也給緩存了。采用這種手段的會(huì)增加我們緩存的維護(hù)成本,需要在插入緩存的時(shí)候刪除這個(gè)空緩存,當(dāng)然我們可以通過設(shè)置較短的超時(shí)時(shí)間來解決這個(gè)問題。
數(shù)據(jù)過濾
- 小數(shù)據(jù)用BitMap,大數(shù)據(jù)可以用布隆過濾器
數(shù)據(jù)一致性問題
我們通常說的數(shù)據(jù)一致性指的是在程序運(yùn)行過程中本地緩存、分布式緩存、mysql數(shù)據(jù)庫三者之間的數(shù)據(jù)一致性
本地緩存與DB保持一致
解決方案
MQ 方案
- 應(yīng)用實(shí)例1收到請(qǐng)求,更新 db,同時(shí)更新應(yīng)用自己的本地緩存.
- 應(yīng)用實(shí)例1 發(fā)送更新 mq 廣播消息.
- 應(yīng)用 實(shí)例2 和應(yīng)用實(shí)例3 收到消息,查詢 db,更新本地緩存.
- 這個(gè)時(shí)候應(yīng)用實(shí)例1,2,3與 DB 數(shù)據(jù)就保持一致
Redis與DB保持一致
基于 binlog 方案
- 更新 db 數(shù)據(jù)
- 監(jiān)聽 mysql binlog, 并寫入到MQ
- 啟動(dòng)一個(gè)數(shù)據(jù)處理應(yīng)用,消費(fèi) MQ 數(shù)據(jù)并進(jìn)行數(shù)據(jù)加工
- 將加工后的數(shù)據(jù)寫入 redis
- 查詢 redis 數(shù)據(jù)返回
延遲雙刪方案
先進(jìn)行緩存清除,再執(zhí)行 update sql,最后(延遲 N 秒)再執(zhí)行緩存清除。
上述中(延遲 N 秒)的時(shí)間要大于一次寫操作的時(shí)間,一般為 3-5 秒。
基于定時(shí)任務(wù)方案
1.更新 db 數(shù)據(jù),同時(shí)寫入數(shù)據(jù)到 redis
2.啟動(dòng)一個(gè)定時(shí)任務(wù)定時(shí)將 db 數(shù)據(jù)同步到 redis
熱key和大key問題
熱key
熱key是服務(wù)端的常見問題,指一段時(shí)間內(nèi)某個(gè)key的訪問量遠(yuǎn)遠(yuǎn)超過其他的key,導(dǎo)致大量訪問流量落在某一個(gè)redis實(shí)例中;或者是帶寬使用率集中在特定的key
以被請(qǐng)求頻率來定義是否是熱key,沒有固定經(jīng)驗(yàn)值。某個(gè)key被高頻訪問導(dǎo)致系統(tǒng)穩(wěn)定性變差,都可以定義為熱key。
可能造成的問題
- 占用大量的CPU資源,影響其他請(qǐng)求并導(dǎo)致整體性能降低。
- 集群架構(gòu)下,產(chǎn)生訪問傾斜,即某個(gè)數(shù)據(jù)分片被大量訪問,而其他數(shù)據(jù)分片處于空閑狀態(tài),可能引起該數(shù)據(jù)分片的連接數(shù)被耗盡,新的連接建立請(qǐng)求被拒絕等問題。
- 在搶購或秒殺場景下,可能因商品對(duì)應(yīng)庫存Key的請(qǐng)求量過大,超出Redis處理能力造成超賣。
- 熱Key的請(qǐng)求壓力數(shù)量超出Redis的承受能力易造成緩存擊穿,即大量請(qǐng)求將被直接指向后端的存儲(chǔ)層,導(dǎo)致存儲(chǔ)訪問量激增甚至宕機(jī),從而影響其他業(yè)務(wù)。
發(fā)現(xiàn)方法
開發(fā)獨(dú)立的熱 key 檢測系統(tǒng)
提供單獨(dú)的熱 key 檢測的接入 sdk,應(yīng)用系統(tǒng)引入該 sdk 后,熱 key 檢測系統(tǒng)自動(dòng)計(jì)
算是否熱 key 并推送相關(guān)結(jié)果給應(yīng)用系統(tǒng),應(yīng)用系統(tǒng)根據(jù)業(yè)務(wù)實(shí)際情況進(jìn)行相應(yīng)處理。
改寫 redis 客戶端收集上報(bào)數(shù)據(jù)
改寫 Redis SDK,記錄每個(gè)請(qǐng)求,定時(shí)把收集到的數(shù)據(jù)上報(bào),然后由一個(gè)統(tǒng)一的服務(wù)進(jìn)行聚合計(jì)算。
解決方案
利用本地緩存
在你發(fā)現(xiàn)熱 key 以后,把熱 key 加載到系統(tǒng)的內(nèi)存中。針對(duì)這種熱 key 請(qǐng)求,會(huì)直接從內(nèi)存中取,而不會(huì)走到 redis 層。
- 優(yōu)點(diǎn):內(nèi)存訪問和 redis 訪問的速度不在一個(gè)量級(jí),基于本地緩存,接口性能非常好, 可以
大大增加單實(shí)例的 QPS。 - 缺點(diǎn):受應(yīng)用內(nèi)存限制,容量有限,數(shù)據(jù)量非常大的時(shí)候,占用太多內(nèi)存,不太適合。部分熱點(diǎn)數(shù)據(jù),需要提前預(yù)知。熱點(diǎn)數(shù)據(jù)自動(dòng)檢測有一定的延遲,系統(tǒng)短時(shí)間內(nèi)承受的風(fēng)險(xiǎn)比較大。
大key
大key是指當(dāng)redis的字符串類型占用內(nèi)存過大或非字符串類型元素?cái)?shù)量過多
生產(chǎn)環(huán)境中,綜合衡量運(yùn)維和環(huán)境的情況,給大key定義參考值如下:
- string類型的key超過10KB
- hash/set/zset/list等數(shù)據(jù)結(jié)構(gòu)中元素個(gè)數(shù)大于5k/整體占用內(nèi)存大于10MB
可能造成的問題
- 客戶端執(zhí)行命令的時(shí)長變慢。
- Redis內(nèi)存達(dá)到maxmemory參數(shù)定義的上限引發(fā)操作阻塞或重要的Key被逐出,甚至引發(fā)內(nèi)存溢出(Out Of Memory)。
- 集群架構(gòu)下,某個(gè)數(shù)據(jù)分片的內(nèi)存使用率遠(yuǎn)超其他數(shù)據(jù)分片,無法使數(shù)據(jù)分片的內(nèi)存資源達(dá)到均衡。
- 對(duì)大Key執(zhí)行讀請(qǐng)求,會(huì)使Redis實(shí)例的帶寬使用率被占滿,導(dǎo)致自身服務(wù)變慢,同時(shí)易波及相關(guān)的服務(wù)。
- 對(duì)大Key執(zhí)行刪除操作,易造成主庫較長時(shí)間的阻塞,進(jìn)而可能引發(fā)同步中斷或主從切換。
發(fā)現(xiàn)方法
實(shí)時(shí)統(tǒng)計(jì)
我們可以通過在Redis 客戶端上實(shí)時(shí)統(tǒng)計(jì)出大Key,直接計(jì)算出Key對(duì)應(yīng)的Value值大小就可以,例如
// b 為序列化之后的數(shù)據(jù) b, err := utils.Serialize(value, c.getSerializer()) if err != nil { return err } // var b []byte // 長度 reqSize = len(b) // 10KB bigKey := 1024 * 10 if reqSize > bigKey { }
- 優(yōu)點(diǎn):對(duì)性能幾乎無影響。
- 缺點(diǎn):返回的Key序列化長度并不等同于它在內(nèi)存空間中的真實(shí)長度,因此不夠準(zhǔn)確,僅可作為參考。
離線全量Key分析
對(duì)Redis的RDB備份文件進(jìn)行定制化的分析,幫助您發(fā)現(xiàn)實(shí)例中的大Key,掌握Key在內(nèi)存中的占用和分布
Redis提供了bigkeys參數(shù)能夠使redis-cli以遍歷的方式分析Redis實(shí)例中的所有Key,并返回Key的整體統(tǒng)計(jì)信息與每個(gè)數(shù)據(jù)類型中Top1的大Key,bigkeys僅能分析并輸入六種數(shù)據(jù)類型(STRING、LIST、HASH、SET、ZSET、STREAM),命令示例為
redis-cli -h 127.0.0.1 -p 6379 --bigkeys
優(yōu)點(diǎn):可對(duì)歷史備份數(shù)據(jù)進(jìn)行分析,對(duì)線上服務(wù)無影響。
缺點(diǎn):時(shí)效性差,RDB文件較大時(shí)耗時(shí)較長。
解決方案
- 業(yè)務(wù)拆分,將key的含義更細(xì)粒度化,避免大key出現(xiàn)。
- 數(shù)據(jù)結(jié)構(gòu)上拆分。如果大key是個(gè)大json,可以通過mset的方式,將這個(gè)key的內(nèi)容打散到各個(gè)實(shí)例中,減小大key對(duì)數(shù)據(jù)量傾斜的影響;如果是大list,可以拆成list_1,list_2,list_N;其他數(shù)據(jù)結(jié)構(gòu)同理。(可以考慮增加單獨(dú)key存儲(chǔ)大key被拆分的個(gè)數(shù)或元數(shù)據(jù)信息)
- 對(duì)于長文本,更建議使用文檔型數(shù)據(jù)庫例如MongoDB等。
- 對(duì)一致性要求不高的場景,嘗試使用客戶端緩存。(只解決了redis的阻塞問題,但機(jī)器或局域網(wǎng)的帶寬問題沒有改善)
- 對(duì)大key的壓縮。相當(dāng)于用cpu資源來降低網(wǎng)絡(luò)io,其中g(shù)oogle提出的snappy算法較常用。
- 對(duì)于hash等數(shù)據(jù)結(jié)構(gòu),需要注意業(yè)務(wù)是否可以引入定期清理無效field的機(jī)制。
- Hash 結(jié)構(gòu)不建議使用,沒有辦法對(duì)具體的Key做過期時(shí)間設(shè)置,只能再額外開發(fā)功能去做,增加開發(fā)成本
到此這篇關(guān)于一文了解發(fā)現(xiàn)并解決Redis熱key與大key問題的文章就介紹到這了,更多相關(guān)Redis熱key與大key內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot集成redis錯(cuò)誤問題及解決方法
這篇文章主要介紹了SpringBoot集成redis錯(cuò)誤問題,本文給大家分享完美解決方法,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02如何利用 Redis 實(shí)現(xiàn)接口頻次限制
這篇文章主要介紹了如何利用 Redis 實(shí)現(xiàn)接口頻次限制,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02redis分布式鎖之可重入鎖的實(shí)現(xiàn)代碼
相信大家都知道可重入鎖的作用防止在同一線程中多次獲取鎖而導(dǎo)致死鎖發(fā)生,本文通過幾個(gè)例子給大家分享redis分布式鎖之可重入鎖的實(shí)現(xiàn)代碼,對(duì)redis分布式鎖的相關(guān)知識(shí),感興趣的朋友一起看看吧2021-05-05