Redis緩存問題與緩存更新機(jī)制詳解
一、緩存問題
1.1 緩存穿透
1.1.1 問題來源
緩存穿透是指緩存和數(shù)據(jù)庫中都沒有的數(shù)據(jù),而用戶不斷發(fā)起請求。由于緩存是不命中時(shí)被動(dòng)寫的,并且出于容錯(cuò)考慮,如果從存儲(chǔ)層查不到數(shù)據(jù)則不寫入緩存,這將導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請求都要到存儲(chǔ)層去查詢,失去了緩存的意義。在流量大時(shí),可能DB就掛掉了,要是有人利用不存在的key頻繁攻擊我們的應(yīng)用,這就是漏洞。
1.1.2 解決方案
1.1.2.1 緩存空對象
- 從緩存取不到的數(shù)據(jù),在數(shù)據(jù)庫中也沒有取到,這時(shí)也可以將key-value對寫為key-null,緩存有效時(shí)間可以設(shè)置短點(diǎn),如30秒(設(shè)置太長會(huì)導(dǎo)致正常情況也沒法使用)。
- 這樣可以防止攻擊用戶反復(fù)用同一個(gè)id暴力攻擊。
1.1.2.2 使用布隆過濾器
- 類似于一個(gè)hash set,用于快速判某個(gè)元素是否存在于集合中,其典型的應(yīng)用場景就是快速判斷一個(gè)key是否存在于某容器,不存在就直接返回。
- 布隆過濾器的關(guān)鍵就在于hash算法和容器大小。
1.2 緩存擊穿
1.2.1 問題來源
緩存擊穿是指緩存某些熱點(diǎn)數(shù)據(jù)失效(一般是緩存時(shí)間到期),這時(shí)由于并發(fā)用戶特別多,同時(shí)讀緩存沒讀到數(shù)據(jù),又同時(shí)去數(shù)據(jù)庫去取數(shù)據(jù),引起數(shù)據(jù)庫壓力瞬間增大,造成過大壓力。
1.2.2 解決方案
1.2.2.1 設(shè)置熱點(diǎn)數(shù)據(jù)永遠(yuǎn)不過期
可以在刷緩存時(shí),設(shè)置熱點(diǎn)數(shù)據(jù)不過期。
1.2.2.2 新增后臺(tái)定時(shí)更新緩存線程(邏輯不過期)
后臺(tái)新增一個(gè)緩存更新線程,緩存快要過期前刷新緩存時(shí)間,防止緩存失效。
1.2.2.3 使用分布式互斥鎖
可以使用Redis提供的分布式互斥鎖,保證只有一個(gè)請求查詢數(shù)據(jù)庫和更新緩存,其他請求阻塞等待緩存更新完成后在訪問緩存。
1.2.2.4 接口限流與熔斷,降級
重要的接口一定要做好限流策略,防止用戶惡意刷接口,同時(shí)要降級準(zhǔn)備,當(dāng)接口中的某些服務(wù)不可用時(shí)候,進(jìn)行熔斷,失敗快速返回機(jī)制。
1.3 緩存雪崩
1.3.1 問題來源
緩存雪崩是指Redis緩存不能正常提供服務(wù)了(阻塞、服務(wù)宕機(jī)、大面積緩存失效等造成),導(dǎo)致所有請求都落到了數(shù)據(jù)庫上,增加了數(shù)據(jù)庫壓力或者導(dǎo)致數(shù)據(jù)庫宕機(jī)。
1.3.2 解決方案
1.3.2.1 緩存過期時(shí)間隨機(jī)
緩存數(shù)據(jù)的過期時(shí)間設(shè)置隨機(jī),防止同一時(shí)間大量數(shù)據(jù)過期現(xiàn)象發(fā)生。
1.3.2.2 分布式部署
采用分布式部署方式部署緩存,避免緩存服務(wù)單節(jié)點(diǎn),同時(shí)將熱點(diǎn)數(shù)據(jù)均勻分布在不同的緩存數(shù)據(jù)庫中。
1.3.2.3 設(shè)置熱點(diǎn)數(shù)據(jù)永遠(yuǎn)不過期
可以在刷緩存時(shí),設(shè)置熱點(diǎn)數(shù)據(jù)不過期。
1.3.2.4 接口限流與熔斷,降級
重要的接口一定要做好限流策略,防止用戶惡意刷接口,同時(shí)要降級準(zhǔn)備,當(dāng)接口中的某些服務(wù)不可用時(shí)候,進(jìn)行熔斷,失敗快速返回機(jī)制。
二、緩存更新機(jī)制
2.1 緩存更新策略分類
| 內(nèi)存淘汰 | 超時(shí)剔除 | 主動(dòng)更新 | |
| 說明 | 重要的接口一定要做好限流策略,防止用戶惡意刷接口,同時(shí)要降級準(zhǔn)備,當(dāng)接口中的某些服務(wù)不可用時(shí)候,進(jìn)行熔斷,失敗快速返回機(jī)制。 | 給緩存數(shù)據(jù)添加TTL時(shí)間,到期后自動(dòng)刪除緩存,下次查詢時(shí)更新緩存 | 編寫業(yè)務(wù)邏輯,在修改數(shù)據(jù)的同時(shí),更新緩存 |
| 一致性 | 差 | 一般 | 好 |
| 維護(hù)成本 | 無 | 低 | 高 |
2.2 內(nèi)存淘汰機(jī)制
2.2.1 noeviction
不淘汰,這是默認(rèn)的淘汰策略;
當(dāng)內(nèi)存達(dá)到限制后,寫請求(set)會(huì)返回錯(cuò)誤,讀請求(get)和刪除請求(del)可以繼續(xù)進(jìn)行
2.2.2 volatile-lru
內(nèi)存不足時(shí),在設(shè)置了過期時(shí)間的key中,優(yōu)先刪除最近最少使用的key
2.2.3 volatile-lfu
內(nèi)存不足時(shí),在設(shè)置了過期時(shí)間的key中,優(yōu)先刪除使用頻率最少的key
2.2.4 volatile-ttl
內(nèi)存不足時(shí),在設(shè)置了過期時(shí)間的key中,優(yōu)先刪除存活剩余時(shí)間最少的key
2.2.5 volatile-random
內(nèi)存不足時(shí),在設(shè)置了過期時(shí)間的key中,隨機(jī)刪除某個(gè)key
2.2.6 allkey-lru
內(nèi)存不足時(shí),在全體key范圍內(nèi),優(yōu)先刪除最近最少使用的key
2.2.7 allkey-lfu
內(nèi)存不足時(shí),在全體key范圍內(nèi),優(yōu)先刪除使用頻率最少的key
2.2.8 allkey-random
內(nèi)存不足時(shí),在全體key范圍內(nèi),隨機(jī)刪除某個(gè)key
2.3 超時(shí)剔除
2.3.1 定時(shí)刪除
設(shè)置一個(gè)定時(shí)任務(wù),隨機(jī)抽取部分過期時(shí)間的key,檢查是否過期,過期了就清除掉
2.3.2 惰性刪除
查詢獲取數(shù)據(jù)時(shí),檢查緩存是否過期,過期則刪除,沒過期不刪除
Redis 默認(rèn)采用惰性刪除+定時(shí)刪除結(jié)合的過期策略
2.4 主動(dòng)更新
2.4.1 主動(dòng)更新策略
2.4.1.1 Cache Aside Pattern
- 由緩存的調(diào)用者
- 在更新數(shù)據(jù)庫的同時(shí)更新緩存
2.4.1.2 Read/Write Through Pattern
- 緩存和數(shù)據(jù)庫整合為一個(gè)服務(wù),由服務(wù)來維護(hù)一致性。
- 調(diào)用者調(diào)用服務(wù),不用關(guān)心一致性問題。
2.4.1.3 Write Behind Caching Pattern
調(diào)用者只操作緩存,由其他線程異步的將緩存數(shù)據(jù)持久化到數(shù)據(jù)庫,最終保持一致。
在企業(yè)中使用最多的主動(dòng)更新策略是 Cache Aside Pattern。也就是我們自己編碼來保證數(shù)據(jù)的一致性。
2.4.2 主動(dòng)更新策略需要考慮的三個(gè)問題
2.4.1 刪除緩存還是更新緩存?
- 2.4.1.1 刪除緩存
更新數(shù)據(jù)庫時(shí)讓緩存失效,查詢時(shí)再更新緩存。(延遲加載)一般選擇這個(gè)方案。
這個(gè)方案比較合理一點(diǎn),可以避免過多的無效寫操作,緩存刪除后,只要沒人來查詢這條數(shù)據(jù),數(shù)據(jù)就不會(huì)被寫入緩存,這樣就可以避免大量無效的寫操作
- 2.4.1.2 更新緩存
每次更新數(shù)據(jù)庫都更新緩存,無效寫操作比較多。
這種方式的缺點(diǎn)很明顯,舉個(gè)例子:假如我更新了100次數(shù)據(jù)庫,然后又同時(shí)更新了100次緩存,但是在更新的時(shí)候并沒有人來查這個(gè)數(shù)據(jù),那么我更新這100次緩存好像也沒啥用吧,相當(dāng)于前99次都是無用功,只有最后一次才是有用的。這就是無效寫操作過多的原因。
2.4.2 如何保證緩存與數(shù)據(jù)庫的操作同時(shí)成功或失???
1)單體系統(tǒng),將緩存與數(shù)據(jù)庫操作放在一個(gè)事務(wù)中。
2)分布式系統(tǒng),利用TCC等分布式事務(wù)方案。
2.4.3 先操作緩存還是數(shù)據(jù)庫?
- 2.4.3.1 先刪除緩存,再操作數(shù)據(jù)庫

這種方式存在很明顯的問題,假設(shè)有兩個(gè)并發(fā)操作,線程A更新,線程B查詢。線程A先刪除緩存,然后還沒來得及更新數(shù)據(jù)庫,CPU資源被線程B搶走,線程B查詢緩存發(fā)現(xiàn)沒有命中(因?yàn)橐呀?jīng)被線程A刪除了),查詢數(shù)據(jù)庫,然后把結(jié)果寫入到緩存中。這個(gè)時(shí)候線程A終于搶到CPU資源了,然后更新數(shù)據(jù)庫,此時(shí)就會(huì)造成數(shù)據(jù)不一致問題。
- 2.4.3.2 先操作數(shù)據(jù)庫,再刪除緩存

這種處理方式使用的頻率是最高的,因?yàn)槌鲥e(cuò)的概率非常小,只有一種比較極端的情況才會(huì)出現(xiàn)數(shù)據(jù)一致性問題。
同樣有兩個(gè)并發(fā)請求,線程A查詢、線程B更新,當(dāng)線程A查詢的時(shí)候,緩存剛好失效,然后就去查詢數(shù)據(jù)庫拿到數(shù)據(jù),在準(zhǔn)備寫入緩存的時(shí)候,CPU資源被線程B搶走,線程B開始更新數(shù)據(jù)庫,然后刪除緩存(這一步其實(shí)等于無用,因?yàn)榫彺嬉呀?jīng)過期)。此時(shí)線程A再次獲取到CPU資源,然后寫入緩存,此時(shí)寫入的是更新前的舊數(shù)據(jù),會(huì)產(chǎn)生數(shù)據(jù)一致性問題。
看起來這確實(shí)也是一個(gè)問題,但是我們仔細(xì)分析一下這種情況都需要滿足哪些條件:
- 1)并發(fā)讀寫操作
- 2)讀緩存時(shí),緩存剛好失效
- 3)寫數(shù)據(jù)庫操作要比寫緩存快
寫數(shù)據(jù)庫是操作磁盤,寫緩存是操作內(nèi)存的,所以不太可能會(huì)出現(xiàn)寫磁盤的速度快于寫內(nèi)存的。因此使用這種方式出現(xiàn)數(shù)據(jù)一致性的概率是很小的。
- 2.4.3.3 延時(shí)雙刪策略

延遲雙刪策略是分布式系統(tǒng)中數(shù)據(jù)庫存儲(chǔ)和緩存數(shù)據(jù)保持一致性的常用策略,但它不是強(qiáng)一致。其實(shí)不管哪種方案,都避免不了Redis存在臟數(shù)據(jù)的問題,只能減輕這個(gè)問題,要想徹底解決,得要用到同步鎖和對應(yīng)的業(yè)務(wù)邏輯層面解決。
前面兩種方案的不足點(diǎn)我們進(jìn)行了分析,第二種方式的使用頻率比較高,但是也有一些小缺陷,雖然說發(fā)生的概率很低,但是這個(gè)概率到了線上會(huì)不會(huì)發(fā)生也不好說,所以就有了延時(shí)雙刪策略對第二種方式做補(bǔ)充。
所謂延時(shí)雙刪就是先進(jìn)行緩存清除,再執(zhí)行數(shù)據(jù)庫操作,最后(延遲N秒)再執(zhí)行緩存清除。延遲N秒的時(shí)間要大于一次寫操作的時(shí)間,這個(gè)延時(shí)N秒就是了完善保證第二種策略中不足,可以保證線程A的寫緩存和線程B的修改數(shù)據(jù)庫、刪除緩存都執(zhí)行完畢,然后再刪除緩存一次,就可以保證后面再來的查詢請求可以查詢到最新數(shù)據(jù)。
ps: 一般的延時(shí)時(shí)間設(shè)置為3S左右,具體情況要根據(jù)業(yè)務(wù)場景取最佳值。
2.5 緩存更新機(jī)制總結(jié)
- 1)內(nèi)存淘汰:不用自己維護(hù),利用Redis內(nèi)存淘汰機(jī)制,自動(dòng)刪除部分緩存數(shù)據(jù),這些被刪除的數(shù)據(jù)在下一次被查詢時(shí)更新。這種方式一致性最差。
- 2)超時(shí)剔除:給緩存數(shù)據(jù)加上過期時(shí)間 ,到期后自動(dòng)刪除,下次查詢時(shí)更新,數(shù)據(jù)一致性問題大概率會(huì)出現(xiàn)。維護(hù)成本比較低。
- 3)主動(dòng)更新:編寫業(yè)務(wù)邏輯,在修改數(shù)據(jù)庫的同時(shí)更新緩存,一致性比較好,維護(hù)成本比較高。一般采用先操作數(shù)據(jù)庫再更新緩存的方式。
一般在數(shù)據(jù)一致性要求比較低的場景下可以使用內(nèi)存淘汰機(jī)制,比如商城首頁的分類信息,這些東西基本上是不會(huì)變化的。如果一致性要求比較高,我們可以采用主動(dòng)更新+超時(shí)剔除兜底的方式來處理。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redis內(nèi)存空間占用及避免數(shù)據(jù)丟失的方法
在現(xiàn)代的互聯(lián)網(wǎng)應(yīng)用中,Redis作為一種高性能的內(nèi)存數(shù)據(jù)庫,被廣泛應(yīng)用于緩存、會(huì)話管理和消息隊(duì)列等場景,然而,Redis的內(nèi)存資源是有限的,過多的內(nèi)存占用可能會(huì)導(dǎo)致數(shù)據(jù)丟失所以本文將給大家介紹一下Redis內(nèi)存空間占用及避免數(shù)據(jù)丟失的方法2023-08-08
淺析Redis中String數(shù)據(jù)類型及其底層編碼
這篇文章主要介紹?Redis?中?String?數(shù)據(jù)類型及其底層編碼,文中有詳細(xì)的代碼示例,對大家的工作及學(xué)習(xí)有一定的幫助,需要的朋友可以參考下2023-05-05
一起raid數(shù)據(jù)恢復(fù)及回遷成功的案例
這篇文章主要介紹了一起raid數(shù)據(jù)恢復(fù)及回遷成功的案例,需要的朋友可以參考下2017-04-04
Redis遍歷所有key的兩個(gè)命令(KEYS 和 SCAN)
這篇文章主要介紹了Redis遍歷所有key的兩個(gè)命令(KEYS 和 SCAN),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04

