緩存使用進(jìn)階大全

為什么要用緩存?

適用互聯(lián)網(wǎng)高并發(fā),高性能場景,解決mysql磁盤慢問題

在項目中緩存是如何使用的?

  • 走springboot @CacheConfig
    查詢:先查緩存,有返回,沒有查數(shù)據(jù)庫,set到緩存返回。
    修改:先刪db,后刪redis
  • 單獨業(yè)務(wù)使用
    db,緩存一致性問題,緩存競爭后面討論解決方案

緩存使用有哪些分類,達(dá)成的效果是?

  • 分頁緩存(查詢條件當(dāng)key,時間控制業(yè)務(wù)可以接受方位)
  • 數(shù)據(jù)庫行數(shù)據(jù)緩存(只緩存有用不易改動的列)
  • 業(yè)務(wù)緩存(通過特定業(yè)務(wù)特殊設(shè)置,特殊淘汰)

整個系統(tǒng)大部分查詢?nèi)呔彺?。冷熱?shù)據(jù)動態(tài)設(shè)置,淘汰,最大化利用緩存。

緩存使用不當(dāng)會造成什么后果?

緩存雪崩

是指緩存中數(shù)據(jù)大批量到過期時間,而查詢數(shù)據(jù)量巨大,引起數(shù)據(jù)庫壓力過大甚至宕機

解決方案:

  • 冷熱數(shù)據(jù)區(qū)分,采用不同的實效時間,緩存數(shù)據(jù)的過期時間設(shè)置隨機,防止同一時間大量數(shù)據(jù)過期現(xiàn)象發(fā)生。
  • 如果緩存數(shù)據(jù)庫是分布式部署,將熱點數(shù)據(jù)均勻分布。
  • 設(shè)置熱點數(shù)據(jù)永遠(yuǎn)不過期。

緩存穿透

是指緩存和數(shù)據(jù)庫中都沒有的數(shù)據(jù),而用戶不斷發(fā)起請求,緩存沒有起到壓力緩沖的作用

解決方案:

  • 接口層增加校驗,如用戶鑒權(quán)校驗,id做基礎(chǔ)校驗,id<=0的直接攔截;
  • 從緩存取不到的數(shù)據(jù),在數(shù)據(jù)庫中也沒有取到,這時也可以將key-value對寫為key-null,緩存有效時間可以設(shè)置短點,如30秒(設(shè)置太長會導(dǎo)致正常情況也沒法使用)。這樣可以- 防止攻擊用戶反復(fù)用同一個id暴力攻擊
  • 針對key做布隆過濾器
  • 采用灰度發(fā)布的方式,先接入少量請求,再逐步增加系統(tǒng)的請求數(shù)量,直到全部請求都切換完成
  • 提前緩存預(yù)熱或定時預(yù)熱

緩存擊穿

是指緩存中沒有但數(shù)據(jù)庫中有的數(shù)據(jù)(一般是緩存時間到期),這時由于并發(fā)用戶特別多,同時讀緩存沒讀到數(shù)據(jù),又同時去數(shù)據(jù)庫去取數(shù)據(jù)(sql又慢),引起數(shù)據(jù)庫壓力瞬間增大,造成過大壓力。緩存失效時瞬時的并發(fā)打到數(shù)據(jù)庫

解決方案:

  • 設(shè)置熱點數(shù)據(jù)永遠(yuǎn)不過期
  • 針對key做加互斥鎖 解決-第一次緩存大并發(fā)問題 單jvm級別加雙重鎖double-check, (使用分布式鎖會限制并發(fā)能力,所以使用單jvm級別限制,特殊場景支付除外)

全量緩存

在處理超大規(guī)模并發(fā)的場景時,由于并發(fā)請求的數(shù)量非常大,即使少量的緩存穿透,也有可能打死數(shù)據(jù)庫引發(fā)雪崩效應(yīng)。

解決方案:

  • 對于這種情況,我們可以緩存全量數(shù)據(jù)來徹底避免緩存穿透問題
    canal訂閱binlog異步更新緩存

緩存并發(fā)競爭

多客戶端同時并發(fā)寫一個key,可能本來應(yīng)該先到的數(shù)據(jù)后到了,導(dǎo)致數(shù)據(jù)版本錯了;或者是多客戶端同時獲取一個 key,修改值之后再寫回去,只要順序錯了,數(shù)據(jù)就錯了

  • 如果對這個key操作,不要求順序
    這種情況下,準(zhǔn)備一個分布式鎖,大家去搶鎖,搶到鎖就做set操作即可,比較簡單。
  • 如果對這個key操作,要求順序
    假設(shè)有一個key1,系統(tǒng)A需要將key1設(shè)置為valueA,系統(tǒng)B需要將key1設(shè)置為valueB,系統(tǒng)C需要將key1設(shè)置為valueC.
    期望按照key1的value值按照 valueA-->valueB-->valueC的順序變化。這種時候我們在數(shù)據(jù)寫入數(shù)據(jù)庫的時候,需要保存一個時間戳。假設(shè)時間戳如下
系統(tǒng)A key 1 {valueA  3:00}
系統(tǒng)B key 1 {valueB  3:05}
系統(tǒng)C key 1 {valueC  3:10}

那么,假設(shè)這會系統(tǒng)B先搶到鎖,將key1設(shè)置為{valueB 3:05}。接下來系統(tǒng)A搶到鎖,發(fā)現(xiàn)自己的valueA的時間戳早于緩存中的時間戳,那就不做set操作了。以此類推。

  • 利用隊列,將set方法變成串行訪問也可以

緩存與數(shù)據(jù)庫雙寫不一致,數(shù)據(jù)一致性問題

Cache Aside Pattern 旁路緩存

  • 讀請求:先讀緩存,如果沒有命中,讀數(shù)據(jù)庫,再set回緩存
  • 寫請求:先刪緩存,再刪數(shù)據(jù)庫,

為什么建議淘汰緩存,不修改緩存

在1和2兩個并發(fā)寫發(fā)生時,由于無法保證時序,此時不管先操作緩存還是先操作數(shù)據(jù)庫,都可能出現(xiàn):

  • 請求1先操作數(shù)據(jù)庫,請求2后操作數(shù)據(jù)庫
  • 請求2先set了緩存,請求1后set了緩存
    導(dǎo)致,數(shù)據(jù)庫與緩存之間的數(shù)據(jù)不一致。

Cache Aside Pattern問題

Cache Aside 在高并發(fā)場景下也會出現(xiàn)數(shù)據(jù)不一致。
讀操作A,沒有命中緩存,就會到數(shù)據(jù)庫中取數(shù)據(jù)v1。
此時來了一個寫操作B,將v2寫入數(shù)據(jù)庫,讓緩存失效;
讀操作A在把v1放入緩存,這樣就會造成臟數(shù)據(jù)。因為緩存中是v1,數(shù)據(jù)庫中是v2

解決方案:

  • b線程:讀緩存->未命中->上寫鎖>從db讀數(shù)據(jù)到緩存->釋放鎖;a線程:上寫鎖->寫db->刪除緩存/改緩存->釋放鎖;
  • 看業(yè)務(wù)方能接受多長時間的臟數(shù)據(jù),然后緩存就設(shè)置多久的過期時間。
  • 或者數(shù)據(jù)庫更新成功后,用MQ去通知刷新緩存


    image
  • canal訂閱binlog,終極方案-還可以解決主從庫同步問題


    image
  • 降級或補償方案或兜底方案

redis基礎(chǔ)

5.0.7 內(nèi)存,單線程,c語言,io多路復(fù)用

持久方式:

rdb,aof,混合模式
一分鐘內(nèi)修改1W次
5分鐘內(nèi)修改10次
15分鐘內(nèi)修改1次

淘汰機制:

volatile-lru:從設(shè)置了過期時間的數(shù)據(jù)集中,選擇最近最久未使用的數(shù)據(jù)釋放;
allkeys-lru:從數(shù)據(jù)集中(包括設(shè)置過期時間以及未設(shè)置過期時間的數(shù)據(jù)集中),選擇最近最久未使用的數(shù)據(jù)釋放;
volatile-random:從設(shè)置了過期時間的數(shù)據(jù)集中,隨機選擇一個數(shù)據(jù)進(jìn)行釋放;
allkeys-random:從數(shù)據(jù)集中(包括了設(shè)置過期時間以及未設(shè)置過期時間)隨機選擇一個數(shù)據(jù)進(jìn)行入釋放;
volatile-ttl:從設(shè)置了過期時間的數(shù)據(jù)集中,選擇馬上就要過期的數(shù)據(jù)進(jìn)行釋放操作;
noeviction:不刪除任意數(shù)據(jù)(但redis還會根據(jù)引用計數(shù)器進(jìn)行釋放),這時如果內(nèi)存不夠時,會直接返回錯誤。

allkeys-lru,針對所有 Key,優(yōu)先刪除最近最少使用的 Key;
volatile-lru,針對帶有過期時間的 Key,優(yōu)先刪除最近最少使用的 Key;
volatile-ttl,針對帶有過期時間的 Key,優(yōu)先刪除即將過期的 Key(根據(jù) TTL 的值);
allkeys-lfu(Redis 4.0 以上),針對所有 Key,優(yōu)先刪除最少使用的 Key;
volatile-lfu(Redis 4.0 以上),針對帶有過期時間的 Key,優(yōu)先刪除最少使用的 Key。
范圍有 allkeys 和 volatile兩種,算法有 LRU、TTL 和 LFU 三種,建議volatile-lfu

redis有哪幾種數(shù)據(jù)類型,在項目中分別在哪里使用了?

string
計數(shù)器,點贊數(shù)、收藏數(shù)、分享數(shù),token

hash
圈子id uid:lastUpdateTime(最后發(fā)布時間)   --點贊,發(fā)帖,回復(fù)頻率控制
userId:name:張三;age:13   -- 用戶信息存儲,修改

list
用戶粉絲列表, 用戶點贊列表, 用戶收藏列表, 用戶關(guān)注列表
lrange命令,并發(fā)大的最新200評論分頁常駐內(nèi)存
消息隊列

set
唯一無序 共同關(guān)注的書 | 互相關(guān)注 | 粉絲

是否互關(guān)去重就是利用交集、并集、差集等操作,可以計算共同喜好,全部的喜好,自己獨有的喜好等功能

zset
唯一有序score,群積分排名晉級管理員 | 書排行榜 | top-n原理跳表
stream
代替之前基于list的發(fā)布訂閱

redis集群方案

一主一備,一主多從,哨兵模式,
類codis,redis cluster,旁路探活
并發(fā)海量建議旁路探活式的集群方式

分布式鎖

單jvm級別

與synchronized關(guān)鍵字相比,Lock的使用更靈活,可以有加鎖超時時間、公平性等優(yōu)勢

單機redis分布鎖

set key value EX 60 NX  超時時間根據(jù)業(yè)務(wù)設(shè)計
setnx SET if Not eXists  1:獲取鎖 0:繼續(xù)嘗試獲取(重試次數(shù)+休息時間)
ex過期時間防止鎖一直被占用

三個請求A,B,C同時競爭鎖,被請求A搶先獲得,其他請求只能不斷嘗試獲取鎖(tryLock)
請求A由于業(yè)務(wù)比較復(fù)雜處理時間已經(jīng)超時,所以請求B能夠獲取到鎖
請求A終于完成了自己的業(yè)務(wù),這個時候執(zhí)行了DEL user_id,但是他自己的鎖已經(jīng)失效了,刪除的是請求B鎖。而請求B的業(yè)務(wù)此時并未處理完,所以此處就出現(xiàn)了問題!
通過value來判斷這把鎖是否屬于自己
SET user_id 10086 EX 30 NX
// 處理業(yè)務(wù)中

//業(yè)務(wù)處理完畢
if( (GET user_id) == "XXX" ){
  DEL user_id
}

if( (GET user_id) == "XXX" ){ //獲取到自己鎖后,進(jìn)行取值判斷且判斷為真。此時,這把鎖恰好失效。而另外一個請求恰好獲得key值為user_id的鎖。
此時程序執(zhí)行了了DEL user_id,刪除了別人加的鎖
  DEL user_id
}
保證查詢和刪除的原子性操作,需要引入lua腳本支持
eval()方法可以確保原子性?源于Redis的特性,因為Redis是單線程,在eval命令執(zhí)行Lua代碼的時候,Lua代碼將被當(dāng)成一個命令去執(zhí)行

加鎖時 key 同,value 不同。
釋放鎖時,根據(jù)value判斷,是不是我的鎖,不能釋放別人的鎖。
及時釋放鎖,而不是利用自動超時
鎖超時時間一定要結(jié)合業(yè)務(wù)情況權(quán)衡,過長,過短都不行。
程序異常之處,要捕獲,并釋放鎖。如果需要回滾的,主動做回滾、補償。保證整體的健壯性,一致性
key超時解決:弄個守護(hù)線程進(jìn)行監(jiān)聽key的失效時間,然后在快要失效的時候為期續(xù)命

集群redis分布式鎖

命令先是落到了主庫。假設(shè)這時主庫down了,而這條數(shù)據(jù)還沒來得及同步到從庫,
sentinel將從庫中的一臺選舉為主庫了。這時,我們的新主庫中并沒有mykey這條數(shù)據(jù),若此時另外一個client執(zhí)行 setnx mykey hisvalue , 也會成功,即也能得到鎖。這就意味著,此時有兩個client獲得了鎖
為了解決故障轉(zhuǎn)移情況下的缺陷,Antirez 發(fā)明了 Redlock 算法,使用redlock算法,需要多個redis實例,加鎖的時候,它會想多半節(jié)點發(fā)送 setex mykey myvalue 命令,只要過半節(jié)點成功了,那么就算加鎖成功了。釋放鎖的時候需要想所有節(jié)點發(fā)送del命令。這是一種基于【大多數(shù)都同意】的一種機制

多級緩存實現(xiàn)

nginx+lua,jvm,ZooKeeper(監(jiān)聽修改變動),redis集群

總結(jié):

  • 鎖的顆粒度,范圍盡量要小。推薦無鎖編程(解決熱點沖突),熱點賬戶分而治之
  • 緩存數(shù)據(jù)允許丟失
  • 遵守緩存使用開發(fā)規(guī)范

參考:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容