選單

Redis進階 - 快取問題:一致性、穿擊、穿透、雪崩、汙染等.

推薦學習

“68道 Redis+168道 MySQL”精品面試題(帶解析),你背廢了嗎?

Redis進階 - 快取問題:一致性、穿擊、穿透、雪崩、汙染等.

01 為什麼要理解Redis快取問題?

在高併發的業務場景下,資料庫大多數情況都是使用者併發訪問最薄弱的環節。所以,就需要使用redis做一個緩衝操作,讓請求先訪問到redis,而不是直接訪問Mysql等資料庫。這樣可以大大緩解資料庫的壓力。

當快取庫出現時,必須要考慮如下問題:

快取穿透

快取穿擊

快取雪崩

快取汙染(或者滿了)

快取和資料庫一致性

02 快取穿透

問題來源

快取穿透是指

快取和資料庫中都沒有的資料

,而使用者不斷髮起請求。由於快取是不命中時被動寫的,並且出於容錯考慮,如果從儲存層查不到資料則不寫入快取,這將導致這個不存在的資料每次請求都要到儲存層去查詢,失去了快取的意義。

在流量大時,可能DB就掛掉了,要是有人利用不存在的key頻繁攻擊我們的應用,這就是漏洞。

如發起為id為“-1”的資料或id為特別大不存在的資料。這時的使用者很可能是攻擊者,攻擊會導致資料庫壓力過大。

解決方案

介面層增加校驗,如使用者鑑權校驗,id做基礎校驗,id<=0的直接攔截;

從快取取不到的資料,在資料庫中也沒有取到,這時也可以將key-value對寫為key-null,快取有效時間可以設定短點,如30秒(設定太長會導致正常情況也沒法使用)。這樣可以防止攻擊使用者反覆用同一個id暴力攻擊

布隆過濾器。bloomfilter就類似於一個hash set,用於快速判某個元素是否存在於集合中,其典型的應用場景就是快速判斷一個key是否存在於某容器,不存在就直接返回。布隆過濾器的關鍵就在於hash演算法和容器大小,

03 快取擊穿

問題來源

快取擊穿是指

快取中沒有但資料庫中有的資料

(一般是快取時間到期),這時由於併發使用者特別多,同時讀快取沒讀到資料,又同時去資料庫去取資料,引起資料庫壓力瞬間增大,造成過大壓力。

解決方案

1、設定熱點資料永遠不過期。

2、介面限流與熔斷,降級。重要的介面一定要做好限流策略,防止使用者惡意刷介面,同時要降級準備,當介面中的某些 服務 不可用時候,進行熔斷,失敗快速返回機制。

3、加互斥鎖

04 快取雪崩

問題來源

快取雪崩是指快取中

資料大批次到過期時間,而查詢資料量巨大,引起資料庫壓力過大甚至down機

。和快取擊穿不同的是,快取擊穿指併發查同一條資料,快取雪崩是不同資料都過期了,很多資料都查不到從而查資料庫。

解決方案

快取資料的過期時間設定隨機,防止同一時間大量資料過期現象發生。

如果快取資料庫是分散式部署,將熱點資料均勻分佈在不同的快取資料庫中。

設定熱點資料永遠不過期。

05 快取汙染(或滿了)

快取汙染問題說的是快取中一些只會被訪問一次或者幾次的的資料,被訪問完後,再也不會被訪問到,但這部分資料依然留存在快取中,消耗快取空間。

快取汙染會隨著資料的持續增加而逐漸顯露,隨著服務的不斷執行,快取中會存在大量的永遠不會再次被訪問的資料。快取空間是有限的,如果快取空間滿了,再往快取裡寫資料時就會有額外開銷,影響Redis效能。這部分額外開銷主要是指寫的時候判斷淘汰策略,根據淘汰策略去選擇要淘汰的資料,然後進行刪除操作。

5。1 最大快取設定多大

系統的設計選擇是一個權衡的過程:大容量快取是能帶來效能加速的收益,但是成本也會更高,而小容量快取不一定就起不到加速訪問的效果。一般來說,

我會建議把快取容量設定為總資料量的 15% 到 30%,兼顧訪問效能和記憶體空間開銷

對於 Redis 來說,一旦確定了快取最大容量,比如 4GB,你就可以使用下面這個命令來設定快取的大小了:

CONFIG SET maxmemory 4gb @pdai: 程式碼已經複製到剪貼簿

不過,快取被寫滿是不可避免的, 所以需要資料淘汰策略。

5。2 快取淘汰策略

Redis共支援八種淘汰策略,分別是noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random 和 allkeys-lfu 策略。

怎麼理解呢

?主要看分三類看:

不淘汰

noeviction (v4。0後預設的)

對設定了過期時間的資料中進行淘汰

隨機:volatile-random

ttl:volatile-ttl

lru:volatile-lru

lfu:volatile-lfu

全部資料進行淘汰

隨機:allkeys-random

lru:allkeys-lru

lfu:allkeys-lfu

具體對照下:

noeviction

該策略是Redis的預設策略。在這種策略下,一旦快取被寫滿了,再有寫請求來時,Redis 不再提供服務,而是直接返回錯誤。這種策略不會淘汰資料,所以無法解決快取汙染問題。一般生產環境不建議使用。

其他七種規則都會根據自己相應的規則來選擇資料進行刪除操作。

volatile-random

這個演算法比較簡單,在設定了過期時間的鍵值對中,進行隨機刪除。因為是隨機刪除,無法把不再訪問的資料篩選出來,所以可能依然會存在快取汙染現象,無法解決快取汙染問題。

volatile-ttl

這種演算法判斷淘汰資料時參考的指標比隨即刪除時多進行一步過期時間的排序。Redis在篩選需刪除的資料時,越早過期的資料越優先被選擇。

volatile-lru

LRU演算法:LRU 演算法的全稱是 Least Recently Used,按照最近最少使用的原則來篩選資料。這種模式下會使用 LRU 演算法篩選設定了過期時間的鍵值對。

Redis最佳化的

LRU演算法實現

Redis會記錄每個資料的最近一次被訪問的時間戳。在Redis在決定淘汰的資料時,第一次會隨機選出 N 個數據,把它們作為一個候選集合。接下來,Redis 會比較這 N 個數據的 lru 欄位,把 lru 欄位值最小的資料從快取中淘汰出去。透過隨機讀取待刪除集合,可以讓Redis不用維護一個巨大的連結串列,也不用操作連結串列,進而提升效能。

Redis 選出的資料個數 N,透過 配置引數 maxmemory-samples 進行配置。個數N越大,則候選集合越大,選擇到的最久未被使用的就更準確,N越小,選擇到最久未被使用的資料的機率也會隨之減小。

volatile-lfu

會使用 LFU 演算法選擇設定了過期時間的鍵值對。

LFU 演算法

:LFU 快取策略是在 LRU 策略基礎上,為每個資料增加了一個計數器,來統計這個資料的訪問次數。當使用 LFU 策略篩選淘汰資料時,首先會根據資料的訪問次數進行篩選,把訪問次數最低的資料淘汰出快取。如果兩個資料的訪問次數相同,LFU 策略再比較這兩個資料的訪問時效性,把距離上一次訪問時間更久的資料淘汰出快取。 Redis的LFU演算法實現:

當 LFU 策略篩選資料時,Redis 會在候選集合中,根據資料 lru 欄位的後 8bit 選擇訪問次數最少的資料進行淘汰。當訪問次數相同時,再根據 lru 欄位的前 16bit 值大小,選擇訪問時間最久遠的資料進行淘汰。

Redis 只使用了 8bit 記錄資料的訪問次數,而 8bit 記錄的最大值是 255,這樣在訪問快速的情況下,如果每次被訪問就將訪問次數加一,很快某條資料就達到最大值255,可能很多資料都是255,那麼退化成LRU演算法了。所以Redis為了解決這個問題,實現了一個更優的計數規則,並可以透過配置項,來控制計數器增加的速度。

引數

lfu-log-factor ,用計數器當前的值乘以配置項 lfu_log_factor 再加 1,再取其倒數,得到一個 p 值;然後,把這個 p 值和一個取值範圍在(0,1)間的隨機數 r 值比大小,只有 p 值大於 r 值時,計數器才加 1。

lfu-decay-time, 控制訪問次數衰減。LFU 策略會計算當前時間和資料最近一次訪問時間的差值,並把這個差值換算成以分鐘為單位。然後,LFU 策略再把這個差值除以 lfu_decay_time 值,所得的結果就是資料 counter 要衰減的值。

lfu-log-factor設定越大,遞增機率越低,lfu-decay-time設定越大,衰減速度會越慢。

我們在應用 LFU 策略時,一般可以將 lfu_log_factor 取值為 10。 如果業務應用中有短時高頻訪問的資料的話,建議把 lfu_decay_time 值設定為 1。可以快速衰減訪問次數。

volatile-lfu 策略是 Redis 4。0 後新增。

allkeys-lru

使用 LRU 演算法在所有資料中進行篩選。具體LFU演算法跟上述 volatile-lru 中介紹的一致,只是篩選的資料範圍是全部快取,這裡就不在重複。

allkeys-random

從所有鍵值對中隨機選擇並刪除資料。volatile-random 跟 allkeys-random演算法一樣,隨機刪除就無法解決快取汙染問題。

allkeys-lfu

使用 LFU 演算法在所有資料中進行篩選。具體LFU演算法跟上述 volatile-lfu 中介紹的一致,只是篩選的資料範圍是全部快取,這裡就不在重複。

allkeys-lfu 策略是 Redis 4。0 後新增。

06 資料庫和快取一致性

問題來源

使用redis做一個緩衝操作,讓請求先訪問到redis,而不是直接訪問MySQL等資料庫:

Redis進階 - 快取問題:一致性、穿擊、穿透、雪崩、汙染等.

讀取快取步驟一般沒有什麼問題,但是一旦涉及到資料更新:資料庫和快取更新,就容易出現快取(Redis)和資料庫(MySQL)間的資料一致性問題。

不管是先寫MySQL資料庫,再刪除Redis快取;還是先刪除快取,再寫庫,都有可能出現數據不一致的情況

。舉一個例子:

1。如果刪除了快取Redis,還沒有來得及寫庫MySQL,另一個執行緒就來讀取,發現快取為空,則去資料庫中讀取資料寫入快取,此時快取中為髒資料。

2。如果先寫了庫,在刪除快取前,寫庫的執行緒宕機了,沒有刪除掉快取,則也會出現資料不一致情況。

因為寫和讀是併發的,沒法保證順序,就會出現快取和資料庫的資料不一致的問題。

6。1 4種相關模式

更新快取的的Design Pattern有四種:Cache aside, Read through, Write through, Write behind caching; 我強烈建議你看看這篇,左耳朵耗子的文章:快取更新的套路 (opens new window)

節選最最常用的Cache Aside Pattern, 總結來說就是

讀的時候

,先讀快取,快取沒有的話,就讀資料庫,然後取出資料後放入快取,同時返回響應。

更新的時候

,先更新資料庫,然後再刪除快取。

其具體邏輯如下:

失效

:應用程式先從cache取資料,沒有得到,則從資料庫中取資料,成功後,放到快取中。

命中

:應用程式從cache中取資料,取到後返回。

更新

:先把資料存到資料庫中,成功後,再讓快取失效。

Redis進階 - 快取問題:一致性、穿擊、穿透、雪崩、汙染等.

注意,我們的更新是先更新資料庫,成功後,讓快取失效。那麼,這種方式是否可以沒有文章前面提到過的那個問題呢?我們可以腦補一下。

一個是查詢操作,一個是更新操作的併發,首先,沒有了刪除cache資料的操作了,而是先更新了資料庫中的資料,此時,快取依然有效,所以,併發的查詢操作拿的是沒有更新的資料,但是,更新操作馬上讓快取的失效了,後續的查詢操作再把資料從資料庫中拉出來。而不會像文章開頭的那個邏輯產生的問題,後續的查詢操作一直都在取老的資料。

這是標準的design pattern,包括Facebook的論文《Scaling Memcache at Facebook (opens new window)》也使用了這個策略。為什麼不是寫完資料庫後更新快取?你可以看一下Quora上的這個問答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend? (opens new window)》,主要是怕兩個併發的寫操作導致髒資料。

那麼,是不是Cache Aside這個就不會有併發問題了?不是的,比如,一個是讀操作,但是沒有命中快取,然後就到資料庫中取資料,此時來了一個寫操作,寫完資料庫後,讓快取失效,然後,之前的那個讀操作再把老的資料放進去,所以,會造成髒資料。

但,這個case理論上會出現,不過,實際上出現的機率可能非常低,因為這個條件需要發生在讀快取時快取失效,而且併發著有一個寫操作。而實際上資料庫的寫操作會比讀操作慢得多,而且還要鎖表,而讀操作必須在寫操作前進入資料庫操作,而又要晚於寫操作更新快取,所有的這些條件都具備的機率基本並不大。

所以,這也就是Quora上的那個答案裡說的,要麼透過2PC或是Paxos協議保證一致性,要麼就是拼命的降低併發時髒資料的機率,而Facebook使用了這個降低機率的玩法,因為2PC太慢,而Paxos太複雜。當然,最好還是為快取設定上過期時間。

6。2 方案:佇列 + 重試機制

Redis進階 - 快取問題:一致性、穿擊、穿透、雪崩、汙染等.

流程如下所示

更新資料庫資料;

快取因為種種問題刪除失敗

將需要刪除的key傳送至訊息佇列

自己消費訊息,獲得需要刪除的key

繼續重試刪除操作,直到成功

然而,該方案有一個缺點,對業務線程式碼造成大量的侵入。於是有了方案二,在方案二中,啟動一個訂閱程式去訂閱資料庫的binlog,獲得需要操作的資料。在應用程式中,另起一段程式,獲得這個訂閱程式傳來的資訊,進行刪除快取操作。

6。3 方案:非同步更新快取(基於訂閱binlog的同步機制)

Redis進階 - 快取問題:一致性、穿擊、穿透、雪崩、汙染等.

技術整體思路

MySQL binlog增量訂閱消費+訊息佇列+增量資料更新到redis

1)讀Redis:熱資料基本都在Redis

2)寫MySQL: 增刪改都是操作MySQL

3)更新Redis資料:MySQ的資料操作binlog,來更新到Redis

Redis更新

1)

資料操作

主要分為兩大塊:

一個是全量(將全部資料一次寫入到redis)

一個是增量(實時更新)

這裡說的是增量,指的是mysql的update、insert、delate變更資料。

2)

讀取binlog後分析 ,利用訊息佇列,推送更新各臺的redis快取資料

這樣一旦MySQL中產生了新的寫入、更新、刪除等操作,就可以把binlog相關的訊息推送至Redis,Redis再根據binlog中的記錄,對Redis進行更新。

其實這種機制,很類似MySQL的主從備份機制,因為MySQL的主備也是透過binlog來實現的資料一致性。

這裡可以結合使用canal(阿里的一款開源框架),透過該框架可以對MySQL的binlog進行訂閱,而canal正是模仿了mysql的slave資料庫的備份請求,使得Redis的資料更新達到了相同的效果。

當然,這裡的訊息推送工具你也可以採用別的第三方:kafka、rabbitMQ等來實現推送更新Redis。

原文連結:https://pdai。tech/md/db/nosql-redis/db-redis-x-cache。html