## **緩存穿透、擊穿、雪崩、預熱、更新、降級**
### 1.緩存穿透
當查詢**Redis中沒有的數據時,該查詢會下沉到數據庫層,同時數據庫層也沒有該數據**,當這種情況大量出現或被惡意攻擊時,接口的訪問全部透過Redis訪問數據庫,而數據庫中也沒有這些數據,我們稱這種現象為"緩存穿透"。緩存穿透會穿透Redis的保護,提升底層數據庫的負載壓力,同時這類穿透查詢沒有數據返回也造成了網絡和計算資源的浪費。

**解決方案:**
1. 在接口訪問層對用戶做校驗,如接口傳參、登陸狀態、n秒內訪問接口的次數;
2. 利用布隆過濾器,將數據庫層有的數據key存儲在位數組中,以判斷訪問的key在底層數據庫中是否存在;
第一種解決方案很好理解,這里介紹一下第二種方案,在前一篇文章中我們介紹了[Redis的布隆過濾器](https://blog.csdn.net/wsdc0521/article/details/107220367),我們知道布隆過濾器可以判斷key一定不在集合內以及key極有可能在集合內。
基于布隆過濾器,我們可以先將數據庫中數據的key存儲在布隆過濾器的位數組中,每次客戶端查詢數據時先訪問Redis:
* 如果Redis內不存在該數據,則通過布隆過濾器判斷數據是否在底層數據庫內;
* 如果布隆過濾器告訴我們該key在底層庫內不存在,則直接返回null給客戶端即可,避免了查詢底層數據庫的動作;
* 如果布隆過濾器告訴我們該key極有可能在底層數據庫內存在,那么將查詢下推到底層數據庫即可;

> 布隆過濾器有誤判率,雖然不能完全避免數據穿透的現象,但已經可以將99.99%的穿透查詢給屏蔽在Redis層了,極大的降低了底層數據庫的壓力,減少了資源浪費。
### 2.緩存擊穿
緩存擊穿和緩存穿透從名詞上可能很難區分開來,它們的區別是:穿透表示底層數據庫沒有數據且緩存內也沒有數據,擊穿表示底層**數據庫有數據而緩存內沒有數據**。當熱點數據key從緩存內失效時,大量訪問同時請求這個數據,就會將查詢下沉到數據庫層,此時數據庫層的負載壓力會驟增,我們稱這種現象為"緩存擊穿"。

**解決方案:**
1. 延長熱點key的過期時間或者設置永不過期,如排行榜,首頁等一定會有高并發的接口;
2. 利用互斥鎖保證同一時刻只有一個客戶端可以查詢底層數據庫的這個數據,一旦查到數據就緩存至Redis內,避免其他大量請求同時穿過Redis訪問底層數據庫;

在使用互斥鎖的時候需要**避免出現死鎖或者鎖過期**的情況:
* 使用前面文章介紹過的lua腳本或事務將獲取鎖和設置過期時間作為一個原子性操作(如:*set kk vv nx px 30000*),以避免出現某個客戶端獲取鎖之后宕機導致的鎖不被釋放造成死鎖現象;
* 另起一個線程監控獲取鎖的線程的查詢狀態,快到鎖過期時間時還沒查詢結束則延長鎖的過期時間,避免多次查詢多次鎖過期造成計算資源的浪費;
### 3.緩存雪崩
緩存雪崩是緩存擊穿的"大面積"版,緩存擊穿是數據庫緩存到Redis內的熱點數據失效導致大量并發查詢穿過redis直接擊打到底層數據庫,而**緩存雪崩是指Redis中大量的key幾乎同時過期**,然后大量并發查詢穿過redis擊打到底層數據庫上,此時數據庫層的負載壓力會驟增,我們稱這種現象為"緩存雪崩"。事實上緩存雪崩相比于緩存擊穿更容易發生,對于大多數公司來講,同時超大并發量訪問同一個過時key的場景的確太少見了,而大量key同時過期,大量用戶訪問這些key的幾率相比緩存擊穿來說明顯更大。

**解決方案:**
1. 在可接受的時間范圍內隨機設置key的過期時間,分散key的過期時間,以防止大量的key在同一時刻過期;
2. 對于一定要在固定時間讓key失效的場景(例如每日12點準時更新所有最新排名),可以在固定的失效時間時在接口服務端設置隨機延時,將請求的時間打散,讓一部分查詢先將數據緩存起來;
3. 延長熱點key的過期時間或者設置永不過期,這一點和緩存擊穿中的方案一樣;

?
### 4.緩存預熱
緩存預熱如字面意思,當系統上線時,緩存內還沒有數據,如果直接提供給用戶使用,每個請求都會穿過緩存去訪問底層數據庫,如果并發大的話,很有可能在上線當天就會宕機,因此我們需要在上線前先將數據庫內的熱點數據緩存至Redis內再提供出去使用,這種操作就成為"緩存預熱"。
緩存預熱的實現方式有很多,比較通用的方式是寫個批任務,在啟動項目時或定時去觸發將底層數據庫內的熱點數據加載到緩存內。
### 5.緩存更新
緩存服務(Redis)和數據服務(底層數據庫)是相互獨立且異構的系統,在更新緩存或更新數據的時候無法做到原子性的同時更新兩邊的數據,因此在并發讀寫或第二步操作異常時會遇到各種數據不一致的問題。如何解決并發場景下更新操作的雙寫一致是緩存系統的一個重要知識點。
> **第二步操作異常**:緩存和數據的操作順序中,第二個動作報錯。如數據庫被更新,?此時失效緩存的時候出錯,緩存內數據仍是舊版本;
**緩存更新的設計模式**有四種:
**Cache aside**:**查詢**:先查緩存,緩存沒有就查數據庫,然后加載至緩存內;**更新**:先更新數據庫,然后讓緩存失效;或者先失效緩存然后更新數據庫;?
**Read through**:在查詢操作中更新緩存,即當緩存失效時,Cache Aside 模式是由調用方負責把數據加載入緩存,而 Read Through 則用緩存服務自己來加載;
**Write through**:在更新數據時發生。當有數據更新的時候,如果沒有命中緩存,直接更新數據庫,然后返回。如果命中了緩存,則更新緩存,然后由緩存自己更新數據庫;?
**Write behind caching**:俗稱write back,在更新數據的時候,只更新緩存,不更新數據庫,緩存會異步地定時批量更新數據庫;
**Cache aside:**
* 為了避免在并發場景下,多個請求同時更新同一個緩存導致臟數據,因此**不能直接更新緩存而是另緩存失效**。
* **先更新數據庫后失效緩存**:并發場景下,推薦使用延遲失效(寫請求**完成后**給緩存設置1s過期時間),在讀請求緩存數據時若redis內已有該數據(其他寫請求還未結束)則不更新。當redis內沒有該數據的時候(其他寫請求已另該緩存失效),讀請求才會更新redis內的數據。這里的讀請求緩存數據可以加上失效時間,以防第二步操作異常導致的不一致情況。
* **先失效緩存后更新數據庫**:并發場景下,推薦使用延遲失效(寫請求**開始前**給緩存設置1s過期時間),在寫請求失效緩存時設置一個1s延遲時間,然后再去更新數據庫的數據,此時其他讀請求仍然可以讀到緩存內的數據,當數據庫端更新完成后,緩存內的數據已失效,之后的讀請求會將數據庫端最新的數據加載至緩存內保證緩存和數據庫端數據一致性;在這種方案下,第二步操作異常不會引起數據不一致,例如設置了緩存1s后失效,然后在更新數據庫時報錯,即使緩存失效,之后的讀請求仍然會把更新前的數據重新加載到緩存內。
> 推薦使用先失效緩存,后更新數據庫,配合延遲失效來更新緩存的模式;

**四種緩存更新模式的優缺點**:
* **Cache Aside**:實現起來較簡單,但需要維護兩個數據存儲,一個是緩存(Cache),一個是數據庫(Repository);
* **Read/Write Through**:只需要維護一個數據存儲(緩存),但是實現起來要復雜一些;
* **Write Behind Caching**:與Read/Write Through 類似,區別是Write Behind Caching的數據持久化操作是異步的,但是Read/Write Through 更新模式的數據持久化操作是同步的。優點是直接操作內存速度快,多次操作可以合并持久化到數據庫。缺點是數據可能會丟失,例如系統斷電等。
> 緩存本身就是通過犧牲強一致性來提高性能,因此使用緩存提升性能,就會有數據更新的延遲性。這就需要我們在評估需求和設計階段根據實際場景去做權衡了。
### 6.緩存降級
緩存降級是指當訪問量劇增、服務出現問題(如響應時間慢或不響應)或非核心服務影響到核心流程的性能時,即使是有損部分其他服務,仍然需要保證主服務可用。可以將其他次要服務的數據進行緩存降級,從而提升主服務的穩定性。
降級的目的是保證核心服務可用,即使是有損的。如去年雙十一的時候淘寶購物車無法修改地址只能使用默認地址,這個服務就是被降級了,這里阿里保證了訂單可以正常提交和付款,但修改地址的服務可以在服務器壓力降低,并發量相對減少的時候再恢復。
降級可以根據實時的監控數據進行自動降級也可以配置開關人工降級。是否需要降級,哪些服務需要降級,在什么情況下再降級,取決于大家對于系統功能的取舍。
- PHP篇
- 函數傳值和傳引用的區別
- 簡述PHP的垃圾回收機制
- 簡述CGI、FAST-CGI、PHP-FPM的關系
- 常見正則表達式
- 多進程寫文件,如何保證都寫成功
- php支持回調函數的數組函數
- MySQL篇
- MySQL的兩種存儲引擎區別
- 事務的四大特性
- 數據庫事務隔離級別
- 什么是索引
- 索引有哪些數據結構,優缺點
- 索引的一些潛規則
- SQL的優化方案
- 簡述MySQL的鎖機制
- 死鎖是怎么產生的?怎么解決?
- 簡述MySQL的主從復制過程,延遲問題怎么解決
- 分布式事務的解決方案
- 數據庫中間件MyCat
- Linux篇
- Linux常用命令
- 對日志文件的IP出現的次數進行統計,并顯示次數最多的前5名
- WEB篇
- 跨域是怎么產生的,如何解決跨域
- Redis篇
- redis介紹
- redis和memcached區別
- redis的持久化方案
- 緩存穿透、擊穿、雪崩、預熱、更新、降級
- 網絡篇
- 計算機網絡體系結構
- 簡述TCP的三次握手、四次揮手過程
- UDP、TCP 區別,適用場景
- HTTP常見狀態碼含義
- 設計模式篇
- 單例模式
- 簡單工廠模式
- 抽象工廠模式
- 觀察者模式
- 策略模式
- 注冊模式
- 適配器模式
- 安全篇
- 跨站腳本攻擊(XSS)
- 跨站點請求偽造(CSRF)
- SQL 注入
- 應用層拒絕服務攻擊
- PHP安全
- 運維篇
- docker面試題
- 消息隊列篇
- 架構篇
- 數據結構與算法篇