?????🔥個人主頁:?中草藥
🔥專欄:【中間件】企業級中間件剖析
一、緩存(Cache)
概述
? ? ? ? Redis最主要的應用場景便是作為緩存。緩存(Cache)是一種用于存儲數據副本的技術或組件,目的是提高數據訪問性能、減輕后端數據源負載 。
數據存儲:在靠近數據源或用戶的位置,開辟一塊存儲空間,用于存放常用或熱點數據副本。例如瀏覽器緩存網頁資源(圖片、CSS、JavaScript 文件等),將其存儲在本地磁盤或內存特定區域?
訪問邏輯:當應用程序請求數據時,先檢查緩存中是否有所需數據。若存在(即緩存命中 ),直接從緩存讀取返回,避免對原始數據源(如數據庫、遠程服務器 )的訪問;若不存在(緩存未命中 ),則從原始數據源獲取數據,同時可將數據存入緩存,供后續可能的請求使用 。
在緩存設計中,二八定律體現得十分明顯
通常約 20% 的熱點數據會被頻繁訪問,占據約 80% 的訪問請求量 。
?使用Redis作為緩存
關系性數據庫有一個很大的缺陷,性能不高(進行一次查詢操作小號的系統資源比較多),主要原因有以下幾點:
1)數據庫把數據存儲在硬盤上,硬盤的 IO 速度并不快。尤其是隨機訪問.
2)如果查詢不能命中索引,就需要進行表的遍歷,這就會大大增加硬盤 IO 次數.
3)關系型數據庫對于 SQL 的執行會做一系列的解析,校驗,優化工作.
4)如果是一些復雜查詢,比如聯合查詢,需要進行笛卡爾積操作,效率更是降低很多.
因此,如果訪問數據庫的并發量比較高,對于數據庫的壓力比較大,很容易使數據庫服務器宕機,應對此的策略主要有兩種,實際開發中,這兩種方案往往是會搭配使用的.
開源:引入更多的機器,部署更多的數據庫實例,構成數據庫集群.(主從復制,分庫分表等...)
節流:引入緩存,使用其他的方式保存經常訪問的熱點數據,從而降低直接訪問數據庫的請求數量.
Redis是一個用來作為數據庫緩存的常用方案
Redis 訪問速度比 MySQL 快很多。或者說處理同一個訪問請求,Redis 消耗的系統資源比 MySQL 少很多。因此 Redis 能支持的并發量更大.
- Redis 數據在內存中,訪問內存比硬盤快很多.
- Redis 只是支持簡單的 key-value 存儲,不涉及復雜查詢的那么多限制規則.
緩存的更新策略
緩存的使用存在一個重要問題,那些數據屬于是熱點數據
1)定期生成
????????每隔一定的周期 (比如一天 / 一周 / 一個月), 對于訪問的數據頻次進行統計。挑選出訪問頻次最高的前 N% 的數據,可以通過定時任務來進行觸發
以搜索引擎為例.
????????用戶在搜索引擎中會輸入一個 "查詢詞", 有些詞是屬于高頻的,大家都愛搜,有些詞就屬于低頻的,大家很少搜.
????????搜索引擎的服務器會把哪個用戶什么時間搜了啥詞,都通過日志的方式記錄的明明白白。然后每隔一段時間對這期間的搜索結果進行統計 (日志的數量可能非常巨大,這個統計的過程可能需要使用 hadoop 或者 spark 等方式完成). 從而就可以得到 "高頻詞表".
優點:上述過程,實際上實現起來比較簡單的。過程更可控,方便排查問題.
缺點:實時性不夠。如果出現一些突發性事件,在短時間內,有一些本來不是熱詞的內容,成了熱詞了。新的熱詞就可能給后面的數據庫啥的帶來較大的壓力。
2)實時生成
先給緩存設定容量上限 (可以通過 Redis 配置文件的 maxmemory 參數設定).
接下來把用戶每次查詢:
如果在 Redis 查到了,就直接返回.
如果 Redis 中不存在,就從數據庫查,把查到的結果同時也寫入 Redis.
????????如果緩存已經滿了 (達到上限), 就觸發緩存淘汰策略,把一些 "相對不那么熱門" 的數據淘汰掉,按照上述過程,持續一段時間之后 Redis 內部的數據自然就是 "熱門數據" 了.
Redis緩存淘汰策略
通用的緩存淘汰策略有以下幾種:
FIFO (First In First Out) 先進先出
把緩存中存在時間最久的(也就是先來的數據)淘汰掉。
LRU (Least Recently Used) 淘汰最久未使用的
記錄每個 key 的最近訪問時間,把最近訪問時間最老的 key 淘汰掉。
LFU (Least Frequently Used) 淘汰訪問次數最少的
記錄每個 key 最近一段時間的訪問次數,把訪問次數最少的淘汰掉。
Random 隨機淘汰
從所有的 key 中抽取幸運兒被隨機淘汰掉。
Redis 內置的淘汰策略如下:
-
volatile-lru 當內存不足以容納新寫入數據時,從設置了過期時間的 key 中使用 LRU(最久未使用)算法進行淘汰
-
allkeys-lru 當內存不足以容納新寫入數據時,從所有 key 中使用 LRU(最近最少使用)算法進行淘汰.
-
volatile-lfu 4.0 版本新增,當內存不足以容納新寫入數據時,在過期的 key 中,使用 LFU 算法進行刪除 key.
-
allkeys-lfu 4.0 版本新增,當內存不足以容納新寫入數據時,從所有 key 中使用 LFU 算法進行淘汰.
-
volatile-random 當內存不足以容納新寫入數據時,從設置了過期時間的 key 中,隨機淘汰數據.
-
allkeys-random 當內存不足以容納新寫入數據時,從所有 key 中隨機淘汰數據.
-
volatile-ttl 在設置了過期時間的 key 中,根據過期時間進行淘汰,越早過期的優先被淘汰.(相當于 FIFO, 只不過是局限于過期的 key)
-
noeviction 默認策略,當內存不足以容納新寫入數據時,新寫入操作會報錯.
整體來說 Redis 提供的策略和我們上述介紹的通用策略是基本一致的.只不過 Redis 這里會針對"過期key"和"全部 key"做分別處理
*緩存使用注意事項
緩存預熱(Cache Warm-up)
????????系統重啟或新服務上線時緩存為空,若大量請求涌入,會直接查詢數據庫,導致瞬時壓力過大。
????????緩存預熱是指在系統啟動或高并發場景來臨前,主動將熱點數據加載到緩存中,避免首次請求直接穿透到數據庫。
????????熱點數據可以基于之前介紹的統計的方式生成即可,這份熱點數據不一定非得那么"準確”,只要能幫助MySQL抵擋大部分請求即可.隨著程序運行的推移,緩存的熱點數據會逐漸自動調整,來更適應當前情況.
?緩存穿透(Cache Penetration)
????????緩存穿透是指查詢一個不存在的數據(緩存和數據庫中均無),可能被惡意攻擊利用,導致每次請求都直接訪問數據庫,如果存在很多像這樣的數據,并進行反復查詢,一樣會給MySQL帶來壓力。
原因
-
業務邏輯缺陷:缺少必要的參數校驗工作,導致非法的key也進行了查詢(典型)
-
開發/運維的誤操作:不小心將某一部分數據刪除了
-
惡意攻擊:頻繁請求無效參數(如不存在的 ID)。
解決方案
可以通過 加強監控報警,改進業務邏輯
此外可以通過以下方式降低問題的嚴重性
1、緩存空對象(Null Caching):對不存在的數據也緩存一個空值(設置較短過期時間)。
if (data == null) {redis.set(key, "NULL", 300); // 緩存空值,5分鐘過期
}
2、布隆過濾器(Bloom Filter):在緩存層前加布隆過濾器,快速判斷數據是否存在。
優點:內存占用低。
缺點:存在誤判率(需權衡誤判率和內存)。
布隆過濾器本質是結合了hash+bitmap,以較小的空間開銷,以較快的響應速度,實現針對key是否存在進行判斷
緩存雪崩(Cache Avalanche)·
緩存雪崩是指大量緩存key同時失效,導致所有請求直接訪問數據庫,引發數據庫壓力激增甚至崩潰。
原因
-
緩存過期時間集中:在短時間內,所有緩存設置為相同 TTL(Time-To-Live)。
-
緩存服務故障:Redis 宕機。
解決方案
1、隨機過期時間:在基礎 TTL 上增加隨機值(如?TTL + random(0, 300s)
)。
2、高可用架構:采用 Redis 集群(主從、哨兵、Cluster 模式)避免單點故障。
3、持久化緩存:對部分核心數據設置永不過期,通過后臺更新數據。
緩存擊穿(Cache Breakdown)
緩存擊穿是指某個熱點數據過期時,大量并發請求直接穿透到數據庫,導致數據庫壓力驟增。
解決方案
- 基于統計的方式發現熱點 key,并設置永不過期。
- 進行必要的服務降級,例如訪問數據庫的時候使用分布式鎖,限制同時請求數據庫的并發數。
-
熱點數據永不過期:通過后臺任務定期更新數據。
對比總結
問題 | 觸發條件 | 核心原因 | 解決方案 |
---|---|---|---|
緩存預熱 | 冷啟動或高并發前 | 緩存初始為空 | 提前加載熱點數據 |
緩存穿透 | 查詢不存在的數據 | 數據不存在于緩存和數據庫 | 布隆過濾器、緩存空對象、參數校驗 |
緩存雪崩 | 大量緩存同時失效 | 緩存過期時間集中或服務宕機 | 隨機過期時間、高可用架構 |
緩存擊穿 | 熱點數據過期 | 高并發請求同一熱點數據 | 互斥鎖、邏輯過期時間、永不過期 |
二、Redis分布式鎖
????????在一個分布式的系統中,也會涉及到多個節點訪問同一個公共資源的情況,此時就需要通過 鎖 來做互斥控制,避免,出現類似于"線程安全"的問題而 java 的 synchronized 或者 C++ 的 std:mutex,這樣的鎖都是只能在當前進程中生效,在分布式的這種多個進程多個主機的場景下就無能為力了此時就需要使用到分布式鎖.
????????Redis 分布式鎖是一種利用 Redis 實現跨進程、跨服務器資源互斥訪問的機制,常用于解決分布式系統中的并發競爭問題(如秒殺扣庫存、任務調度等場景)。
分布式鎖的基本實現
原理很簡單:就是通過一個鍵值對來標識鎖的狀態,是一個簡單的互斥鎖
Redis中提供了setnx正好適用這個場景:即key不存在就設置,存在則直接失敗
為了防止出現 當服務器1 加鎖之后,開始處理業務的過程中,如果 服務器1 意外宕機了,就會導致解鎖操作(刪除該key)不能執行.就可能引起其他服務器始終無法獲取到鎖的情況,需要引入過期時間
set nx ex
注意此處設置過期時間爺需要使用一個命令來設置,由于redis的多個命令之間不存在關聯,不是原子的,可能會存在setnx成功后,expire失敗的情況
為了防止出現,由于業務邏輯產生的誤解鎖問題,意外導致服務器1加鎖之后,被服務器2解鎖的情況,需要引入校驗id
????????給不同的服務器進行編號,作為唯一的身份id,且在設置key-value的時候,key是針對需要加鎖的資源,value是持有鎖的服務器編號
????????解鎖的時候, 先查詢一下這個鎖對應的服務器編號然后判定一下這個編號是否就是當前執行解鎖的服務器編號如果是, 才能真正執行 del. 如果不是,就失敗,通過上述校驗,就可以有效避免,"誤解鎖"。
????????在解鎖的操作的時候由于需要 驗證+解鎖 這兩步操作,這兩步操作也不是原子的,會出現問題此時又需要 引入Lua腳本(Redis本身支持Lua作為內嵌腳本)
if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1])
elsereturn 0
end;
一個Lua腳本會被redis服務器作為原子來執行
為了防止出現,在任務還未完成執行的時候,我們設置的key就先過期了,這會導致鎖的提前失效
把key的過期時間設置的足夠長,比如 30s,是否能解決這個問題呢?
????????很明顯,設置多長時間合適,是無止境的.即使設置再長,也不能完全保證就沒有提前失效的情況.而且如果設置的太長了,萬一對應的服務器掛了,此時其他服務器也不能及時的獲取到鎖.因此相比于設置一個固定的長時間,不如動態的調整時間更合適.
引入watch dog(看門狗)
????????“Watchdog 看門狗” 是一種監控機制,在這里通過加鎖服務器(業務服務器非redis服務器)上的一個單獨線程,通過這個線程來對鎖的過期時間進行續約。
舉個具體的例子:
初始情況下設置過期時間為 10s.同時設定看門狗線程每隔 3s 檢測一次那么當 3s 時間到的時候,看門狗就會判定當前任務是否完成.
- 如果任務已經完成,則直接通過 lua 腳本的方式,釋放鎖(刪除 key).
- 如果任務未完成,則把過期時間重寫設置為 10s.(即“續約")
? ? ? ? 實際業務中的 Redis 一般是以集群的方式部署的. 那么就可能出現以下比較極端的情況:
????????服務器 1 向 master 節點進行加鎖操作。這個寫入 key 的過程剛剛完成,master 掛了;slave 節點升級成了新的 master 節點。但是由于剛才寫入的這個 key 尚未來得及同步給 slave 呢,此時就相當于服務器 1 的加鎖操作形同虛設了,服務器 2 仍然可以進行加鎖 (即給新的 master 寫入 key. 因為新的 master 不包含剛才的 key).
????????為了解決這個問題,Redis 的作者提出了 Redlock 算法.
????????Redlock 算法是 Redis 作者 antirez 提出的分布式鎖算法,用于解決分布式環境下鎖的可靠性和有效性問題 ,它的核心是特別是避免在多個 Redis 實例場景中出現單點故障、死鎖和鎖丟失等情況
?
????????引入一組 Redis 節點,其中每一組 Redis 節點都包含一個主節點和若干從節點,并且組和組之間存儲的數據都是一致的,相互之間是"備份"關系(而并非是數據集合的一部分,這點有別于 Redis cluster)加鎖的時候,按照一定的順序,寫多個 master 節點,在寫鎖的時候需要設定操作的"超時時間".比如50ms.即如果 setnx 操作超過了 50ms 還沒有成功,就視為加鎖失敗.
????????如果給某個節點加鎖失敗,就立即再嘗試下一個節點,當加鎖成功的節點數超過總節點數的一半,才視為加鎖成功。同理,在釋放鎖的時候也需要將所有節點都進行解鎖操作(哪怕是之間超時的節點,也要嘗試解鎖,盡量保證邏輯的嚴密)
????????如上圖,一共五個節點,三個加鎖成功,兩個失敗,此時視為加鎖成功,這樣的話,即使有某些節點掛了,也不影響鎖的正確性。
輕則失本,躁則失君。 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ????????????????????????????????????????? ——老子
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部內容啦,若有錯誤疏忽希望各位大佬及時指出💐
? 制作不易,希望能對各位提供微小的幫助,可否留下你免費的贊呢🌸?