目錄
1.什么是Redis緩存問題?
2.緩存穿透
3.緩存擊穿
4.緩存雪崩
5.緩存污染(或滿了)
? ?5.1?最大緩存設(shè)置多大
? ?5.2?緩存淘汰策略
6.數(shù)據(jù)庫和緩存一致性
? ?6.1?4種相關(guān)模式
? ?6.2 方案:隊列+重試機制
? ?6.3?方案:異步更新緩存(基于訂閱binlog的同步機制)
1.什么是Redis緩存問題?
在高并發(fā)的業(yè)務(wù)場景下,數(shù)據(jù)庫大多數(shù)情況都是用戶并發(fā)訪問最薄弱的環(huán)節(jié)。所以,就需要使用redis做一個緩沖操作,讓請求先訪問到redis,而不是直接訪問Mysql等數(shù)據(jù)庫。這樣可以大大緩解數(shù)據(jù)庫的壓力。
當緩存庫出現(xiàn)時,必須要考慮如下問題:
- 緩存穿透
- 緩存穿擊
- 緩存雪崩
- 緩存污染(或者滿了)
- 緩存和數(shù)據(jù)庫一致性
2.緩存穿透
什么是緩存穿透?
緩存穿透說簡單點就是大量請求的 key 是不合理的,根本不存在于緩存中,也不存在于數(shù)據(jù)庫中 。這就導致這些請求直接到了數(shù)據(jù)庫上,根本沒有經(jīng)過緩存這一層,對數(shù)據(jù)庫造成了巨大的壓力,可能直接就被這么多請求弄宕機了。
舉個例子:某個黑客故意制造一些非法的 key 發(fā)起大量請求,導致大量請求落到數(shù)據(jù)庫,結(jié)果數(shù)據(jù)庫上也沒有查到對應(yīng)的數(shù)據(jù)。也就是說這些請求最終都落到了數(shù)據(jù)庫上,對數(shù)據(jù)庫造成了巨大的壓力。
有什么解決辦法嗎?
最基本的就是首先做好參數(shù)校驗,一些不合法的參數(shù)請求直接拋出異常信息返回給客戶端。比如查詢的數(shù)據(jù)庫 id 不能小于 0、傳入的郵箱格式不對的時候直接返回錯誤消息給客戶端等等。
1)緩存無效 key
如果緩存和數(shù)據(jù)庫都查不到某個 key 的數(shù)據(jù)就寫一個到 Redis 中去并設(shè)置過期時間,具體命令如下:SET key value EX 10086
。這種方式可以解決請求的 key 變化不頻繁的情況,如果黑客惡意攻擊,每次構(gòu)建不同的請求 key,會導致 Redis 中緩存大量無效的 key 。很明顯,這種方案并不能從根本上解決此問題。如果非要用這種方式來解決穿透問題的話,盡量將無效的 key 的過期時間設(shè)置短一點比如 1 分鐘。
如果用 Java 代碼展示的話,差不多是下面這樣的:
public Object getObjectInclNullById(Integer id) {
// 從緩存中獲取數(shù)據(jù)
Object cacheValue = cache.get(id);
// 緩存為空
if (cacheValue == null) {
// 從數(shù)據(jù)庫中獲取
Object storageValue = storage.get(key);
// 緩存空對象
cache.set(key, storageValue);
// 如果存儲數(shù)據(jù)為空,需要設(shè)置一個過期時間(300秒)
if (storageValue == null) {
// 必須設(shè)置過期時間,否則有被攻擊的風險
cache.expire(key, 60 * 5);
}
return storageValue;
}
return cacheValue;
}
2)布隆過濾器
布隆過濾器是一個非常神奇的數(shù)據(jù)結(jié)構(gòu),通過它我們可以非常方便地判斷一個給定數(shù)據(jù)是否存在于海量數(shù)據(jù)中。我們需要的就是判斷 key 是否合法,是不是感覺布隆過濾器就是我們想要找的那個正確答案?。
具體是這樣做的:把所有可能存在的請求的值都存放在布隆過濾器中,當用戶請求過來,先判斷用戶發(fā)來的請求的值是否存在于布隆過濾器中。不存在的話,直接返回請求參數(shù)錯誤信息給客戶端,存在的話才會走下面的流程。
加入布隆過濾器之后的緩存處理流程圖如下。
但是,需要注意的是布隆過濾器可能會存在誤判的情況??偨Y(jié)來說就是:布隆過濾器說某個元素存在,小概率會誤判。布隆過濾器說某個元素不在,那么這個元素一定不在。
為什么會出現(xiàn)誤判的情況呢? 我們還要從布隆過濾器的原理來說!
我們先來看一下,當一個元素加入布隆過濾器中的時候,會進行哪些操作:
- 使用布隆過濾器中的哈希函數(shù)對元素值進行計算,得到哈希值(有幾個哈希函數(shù)得到幾個哈希值)。
- 根據(jù)得到的哈希值,在位數(shù)組中把對應(yīng)下標的值置為 1。
我們再來看一下,當我們需要判斷一個元素是否存在于布隆過濾器的時候,會進行哪些操作:
- 對給定元素再次進行相同的哈希計算;
- 得到值之后判斷位數(shù)組中的每個元素是否都為 1,如果值都為 1,那么說明這個值在布隆過濾器中,如果存在一個值不為 1,說明該元素不在布隆過濾器中。
然后,一定會出現(xiàn)這樣一種情況:不同的字符串可能哈希出來的位置相同。 (可以適當增加位數(shù)組大小或者調(diào)整我們的哈希函數(shù)來降低概率)
3.緩存擊穿
什么是緩存擊穿?
緩存擊穿中,請求的 key 對應(yīng)的是 熱點數(shù)據(jù) ,該數(shù)據(jù) 存在于數(shù)據(jù)庫中,但不存在于緩存中(通常是因為緩存中的那份數(shù)據(jù)已經(jīng)過期) 。這就可能會導致瞬時大量的請求直接打到了數(shù)據(jù)庫上,對數(shù)據(jù)庫造成了巨大的壓力,可能直接就被這么多請求弄宕機了。
舉個例子:秒殺進行過程中,緩存中的某個秒殺商品的數(shù)據(jù)突然過期,這就導致瞬時大量對該商品的請求直接落到數(shù)據(jù)庫上,對數(shù)據(jù)庫造成了巨大的壓力。
有什么解決辦法嗎?
- 設(shè)置熱點數(shù)據(jù)永不過期或者過期時間比較長。
- 針對熱點數(shù)據(jù)提前預(yù)熱,將其存入緩存中并設(shè)置合理的過期時間比如秒殺場景下的數(shù)據(jù)在秒殺結(jié)束之前不過期。
- 請求數(shù)據(jù)庫寫數(shù)據(jù)到緩存之前,先獲取互斥鎖,保證只有一個請求會落到數(shù)據(jù)庫上,減少數(shù)據(jù)庫的壓力。
緩存穿透和緩存擊穿有什么區(qū)別?
緩存穿透中,請求的 key 既不存在于緩存中,也不存在于數(shù)據(jù)庫中。
緩存擊穿中,請求的 key 對應(yīng)的是 熱點數(shù)據(jù) ,該數(shù)據(jù) 存在于數(shù)據(jù)庫中,但不存在于緩存中(通常是因為緩存中的那份數(shù)據(jù)已經(jīng)過期) 。?
4.緩存雪崩
什么是緩存雪崩?
實際上,緩存雪崩描述的就是這樣一個簡單的場景:緩存在同一時間大面積的失效,導致大量的請求都直接落到了數(shù)據(jù)庫上,對數(shù)據(jù)庫造成了巨大的壓力。 這就好比雪崩一樣,摧枯拉朽之勢,數(shù)據(jù)庫的壓力可想而知,可能直接就被這么多請求弄宕機了。
另外,緩存服務(wù)宕機也會導致緩存雪崩現(xiàn)象,導致所有的請求都落到了數(shù)據(jù)庫上。
舉個例子:數(shù)據(jù)庫中的大量數(shù)據(jù)在同一時間過期,這個時候突然有大量的請求需要訪問這些過期的數(shù)據(jù)。這就導致大量的請求直接落到數(shù)據(jù)庫上,對數(shù)據(jù)庫造成了巨大的壓力。
有哪些解決辦法?
針對 Redis 服務(wù)不可用的情況:
- 采用 Redis 集群,避免單機出現(xiàn)問題整個緩存服務(wù)都沒辦法使用。
- 限流,避免同時處理大量的請求。
針對熱點緩存失效的情況:
- 設(shè)置不同的失效時間比如隨機設(shè)置緩存的失效時間。
- 緩存永不失效(不太推薦,實用性太差)。
- 設(shè)置二級緩存。
緩存雪崩和緩存擊穿有什么區(qū)別?
緩存雪崩和緩存擊穿比較像,但緩存雪崩導致的原因是緩存中的大量或者所有數(shù)據(jù)失效,緩存擊穿導致的原因主要是某個熱點數(shù)據(jù)不存在與緩存中(通常是因為緩存中的那份數(shù)據(jù)已經(jīng)過期)。?
5.緩存污染(或滿了)
緩存污染問題說的是緩存中一些只會被訪問一次或者幾次的的數(shù)據(jù),被訪問完后,再也不會被訪問到,但這部分數(shù)據(jù)依然留存在緩存中,消耗緩存空間。
緩存污染會隨著數(shù)據(jù)的持續(xù)增加而逐漸顯露,隨著服務(wù)的不斷運行,緩存中會存在大量的永遠不會再次被訪問的數(shù)據(jù)。緩存空間是有限的,如果緩存空間滿了,再往緩存里寫數(shù)據(jù)時就會有額外開銷,影響Redis性能。這部分額外開銷主要是指寫的時候判斷淘汰策略,根據(jù)淘汰策略去選擇要淘汰的數(shù)據(jù),然后進行刪除操作。
? ?5.1?最大緩存設(shè)置多大
系統(tǒng)的設(shè)計選擇是一個權(quán)衡的過程:大容量緩存是能帶來性能加速的收益,但是成本也會更高,而小容量緩存不一定就起不到加速訪問的效果。一般來說,我會建議把緩存容量設(shè)置為總數(shù)據(jù)量的 15% 到 30%,兼顧訪問性能和內(nèi)存空間開銷。
對于 Redis 來說,一旦確定了緩存最大容量,比如 4GB,你就可以使用下面這個命令來設(shè)定緩存的大小了:
不過,緩存被寫滿是不可避免的, 所以需要數(shù)據(jù)淘汰策略。?
? ?5.2?緩存淘汰策略
Redis共支持八種淘汰策略,分別是noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random 和 allkeys-lfu 策略。
怎么理解呢?主要看分三類看:
- 不淘汰
- noeviction (v4.0后默認的)
- 對設(shè)置了過期時間的數(shù)據(jù)中進行淘汰
- 隨機:volatile-random
- ttl:volatile-ttl
- lru:volatile-lru
- lfu:volatile-lfu
- 全部數(shù)據(jù)進行淘汰
- 隨機:allkeys-random
- lru:allkeys-lru
- lfu:allkeys-lfu
具體分析如下:
1.noeviction
該策略是Redis的默認策略。在這種策略下,一旦緩存被寫滿了,再有寫請求來時,Redis 不再提供服務(wù),而是直接返回錯誤。這種策略不會淘汰數(shù)據(jù),所以無法解決緩存污染問題。一般生產(chǎn)環(huán)境不建議使用。
其他七種規(guī)則都會根據(jù)自己相應(yīng)的規(guī)則來選擇數(shù)據(jù)進行刪除操作。
2.volatile-random
這個算法比較簡單,在設(shè)置了過期時間的鍵值對中,進行隨機刪除。因為是隨機刪除,無法把不再訪問的數(shù)據(jù)篩選出來,所以可能依然會存在緩存污染現(xiàn)象,無法解決緩存污染問題。
3.volatile-ttl
這種算法判斷淘汰數(shù)據(jù)時參考的指標比隨機刪除時多進行一步過期時間的排序。Redis在篩選需刪除的數(shù)據(jù)時,越早過期的數(shù)據(jù)越優(yōu)先被選擇。
4.volatile-lru
LRU算法:LRU 算法的全稱是 Least Recently Used,按照最近最少使用的原則來篩選數(shù)據(jù)。這種模式下會使用 LRU 算法篩選設(shè)置了過期時間的鍵值對。
詳細LRU算法可看此博客:LRU緩存淘汰算法詳解與實現(xiàn)_北~笙的博客-CSDN博客
Redis優(yōu)化的LRU算法實現(xiàn):
Redis會記錄每個數(shù)據(jù)的最近一次被訪問的時間戳。在Redis在決定淘汰的數(shù)據(jù)時,第一次會隨機選出 N 個數(shù)據(jù),把它們作為一個候選集合。接下來,Redis 會比較這 N 個數(shù)據(jù)的 lru 字段,把 lru 字段值最小的數(shù)據(jù)從緩存中淘汰出去。通過隨機讀取待刪除集合,可以讓Redis不用維護一個巨大的鏈表,也不用操作鏈表,進而提升性能。
Redis 選出的數(shù)據(jù)個數(shù) N,通過 配置參數(shù) maxmemory-samples 進行配置。個數(shù)N越大,則候選集合越大,選擇到的最久未被使用的就更準確,N越小,選擇到最久未被使用的數(shù)據(jù)的概率也會隨之減小。
5.volatile-lfu
會使用 LFU 算法選擇設(shè)置了過期時間的鍵值對。
LFU 算法:LFU 緩存策略是在 LRU 策略基礎(chǔ)上,為每個數(shù)據(jù)增加了一個計數(shù)器,來統(tǒng)計這個數(shù)據(jù)的訪問次數(shù)。當使用 LFU 策略篩選淘汰數(shù)據(jù)時,首先會根據(jù)數(shù)據(jù)的訪問次數(shù)進行篩選,把訪問次數(shù)最低的數(shù)據(jù)淘汰出緩存。如果兩個數(shù)據(jù)的訪問次數(shù)相同,LFU 策略再比較這兩個數(shù)據(jù)的訪問時效性,把距離上一次訪問時間更久的數(shù)據(jù)淘汰出緩存。 Redis的LFU算法實現(xiàn):
當 LFU 策略篩選數(shù)據(jù)時,Redis 會在候選集合中,根據(jù)數(shù)據(jù) lru 字段的后 8bit 選擇訪問次數(shù)最少的數(shù)據(jù)進行淘汰。當訪問次數(shù)相同時,再根據(jù) lru 字段的前 16bit 值大小,選擇訪問時間最久遠的數(shù)據(jù)進行淘汰。
Redis 只使用了 8bit 記錄數(shù)據(jù)的訪問次數(shù),而 8bit 記錄的最大值是 255,這樣在訪問快速的情況下,如果每次被訪問就將訪問次數(shù)加一,很快某條數(shù)據(jù)就達到最大值255,可能很多數(shù)據(jù)都是255,那么退化成LRU算法了。所以Redis為了解決這個問題,實現(xiàn)了一個更優(yōu)的計數(shù)規(guī)則,并可以通過配置項,來控制計數(shù)器增加的速度。
6.allkeys-lru
使用 LRU 算法在所有數(shù)據(jù)中進行篩選。具體LFU算法跟上述 volatile-lru 中介紹的一致,只是篩選的數(shù)據(jù)范圍是全部緩存,這里就不在重復(fù)。
7.allkeys-random
從所有鍵值對中隨機選擇并刪除數(shù)據(jù)。volatile-random 跟 allkeys-random算法一樣,隨機刪除就無法解決緩存污染問題。
8.allkeys-lfu 使用 LFU 算法在所有數(shù)據(jù)中進行篩選。具體LFU算法跟上述 volatile-lfu 中介紹的一致,只是篩選的數(shù)據(jù)范圍是全部緩存,這里就不在重復(fù)。
allkeys-lfu 策略是 Redis 4.0 后新增。
6.數(shù)據(jù)庫和緩存一致性
- 問題來源
使用redis做一個緩沖操作,讓請求先訪問到redis,而不是直接訪問MySQL等數(shù)據(jù)庫:
讀取緩存步驟一般沒有什么問題,但是一旦涉及到數(shù)據(jù)更新:數(shù)據(jù)庫和緩存更新,就容易出現(xiàn)緩存(Redis)和數(shù)據(jù)庫(MySQL)間的數(shù)據(jù)一致性問題。
不管是先寫MySQL數(shù)據(jù)庫,再刪除Redis緩存;還是先刪除緩存,再寫庫,都有可能出現(xiàn)數(shù)據(jù)不一致的情況。舉一個例子:
1.如果刪除了緩存Redis,還沒有來得及寫庫MySQL,另一個線程就來讀取,發(fā)現(xiàn)緩存為空,則去數(shù)據(jù)庫中讀取數(shù)據(jù)寫入緩存,此時緩存中為臟數(shù)據(jù)。
2.如果先寫了庫,在刪除緩存前,寫庫的線程宕機了,沒有刪除掉緩存,則也會出現(xiàn)數(shù)據(jù)不一致情況。
因為寫和讀是并發(fā)的,沒法保證順序,就會出現(xiàn)緩存和數(shù)據(jù)庫的數(shù)據(jù)不一致的問題。?
? ?6.1?4種相關(guān)模式
更新緩存的的Design Pattern有四種:Cache aside Pattern, Read through, Write through, Write behind caching;?
這里主要來看最常用的Cache Aside Pattern, 總結(jié)來說就是
- 讀的時候,先讀緩存,緩存沒有的話,就讀數(shù)據(jù)庫,然后取出數(shù)據(jù)后放入緩存,同時返回響應(yīng)。
- 更新的時候,先更新數(shù)據(jù)庫,然后再刪除緩存。
其具體邏輯如下:
- 失效:應(yīng)用程序先從cache取數(shù)據(jù),沒有得到,則從數(shù)據(jù)庫中取數(shù)據(jù),成功后,放到緩存中。
- 命中:應(yīng)用程序從cache中取數(shù)據(jù),取到后返回。
- 更新:先把數(shù)據(jù)存到數(shù)據(jù)庫中,成功后,再讓緩存失效。
注意,我們的更新是先更新數(shù)據(jù)庫,成功后,讓緩存失效。那么,這種方式是否可以沒有文章前面提到過的那個問題呢?我們可以腦補一下。
一個是查詢操作,一個是更新操作的并發(fā),首先,沒有了刪除cache數(shù)據(jù)的操作了,而是先更新了數(shù)據(jù)庫中的數(shù)據(jù),此時,緩存依然有效,所以,并發(fā)的查詢操作拿的是沒有更新的數(shù)據(jù),但是,更新操作馬上讓緩存的失效了,后續(xù)的查詢操作再把數(shù)據(jù)從數(shù)據(jù)庫中拉出來。而不會像文章開頭的那個邏輯產(chǎn)生的問題,后續(xù)的查詢操作一直都在取老的數(shù)據(jù)。
那么,是不是Cache Aside這個就不會有并發(fā)問題了?不是的,比如,一個是讀操作,但是沒有命中緩存,然后就到數(shù)據(jù)庫中取數(shù)據(jù),此時來了一個寫操作,寫完數(shù)據(jù)庫后,讓緩存失效,然后,之前的那個讀操作再把老的數(shù)據(jù)放進去,所以,會造成臟數(shù)據(jù)。
但,這個case理論上會出現(xiàn),不過,實際上出現(xiàn)的概率可能非常低,因為這個條件需要發(fā)生在讀緩存時緩存失效,而且并發(fā)著有一個寫操作。而實際上數(shù)據(jù)庫的寫操作會比讀操作慢得多,而且還要鎖表,而讀操作必需在寫操作前進入數(shù)據(jù)庫操作,而又要晚于寫操作更新緩存,所有的這些條件都具備的概率基本并不大。
? ?6.2 方案:隊列+重試機制
流程如下所示
- 更新數(shù)據(jù)庫數(shù)據(jù);
- 緩存因為種種問題刪除失敗
- 將需要刪除的key發(fā)送至消息隊列
- 自己消費消息,獲得需要刪除的key
- 繼續(xù)重試刪除操作,直到成功
然而,該方案有一個缺點,對業(yè)務(wù)線代碼造成大量的侵入。于是有了方案二,在方案二中,啟動一個訂閱程序去訂閱數(shù)據(jù)庫的binlog,獲得需要操作的數(shù)據(jù)。在應(yīng)用程序中,另起一段程序,獲得這個訂閱程序傳來的信息,進行刪除緩存操作。
? ?6.3?方案:異步更新緩存(基于訂閱binlog的同步機制)
整體思路:
MySQL binlog增量訂閱消費+消息隊列+增量數(shù)據(jù)更新到redis
1)讀Redis:熱數(shù)據(jù)基本都在Redis
2)寫MySQL: 增刪改都是操作MySQL
3)更新Redis數(shù)據(jù):MySQ的數(shù)據(jù)操作binlog,來更新到Redis
Redis更新
1)數(shù)據(jù)操作主要分為兩大塊:
- 一個是全量(將全部數(shù)據(jù)一次寫入到redis)
- 一個是增量(實時更新)
這里說的是增量,指的是mysql的update、insert、delate變更數(shù)據(jù)。
2)讀取binlog后分析 ,利用消息隊列,推送更新各臺的redis緩存數(shù)據(jù)。
這樣一旦MySQL中產(chǎn)生了新的寫入、更新、刪除等操作,就可以把binlog相關(guān)的消息推送至Redis,Redis再根據(jù)binlog中的記錄,對Redis進行更新。
其實這種機制,很類似MySQL的主從備份機制,因為MySQL的主備也是通過binlog來實現(xiàn)的數(shù)據(jù)一致性。
這里可以結(jié)合使用canal(阿里的一款開源框架),通過該框架可以對MySQL的binlog進行訂閱,而canal正是模仿了mysql的slave數(shù)據(jù)庫的備份請求,使得Redis的數(shù)據(jù)更新達到了相同的效果。文章來源:http://www.zghlxwxcb.cn/news/detail-661944.html
當然,這里的消息推送工具你也可以采用別的第三方:kafka、rabbitMQ等來實現(xiàn)推送更新Redis。文章來源地址http://www.zghlxwxcb.cn/news/detail-661944.html
到了這里,關(guān)于Redis緩存問題(穿透, 擊穿, 雪崩, 污染, 一致性)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!