為什么要用緩存?
適用互聯(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去通知刷新緩存
imagecanal訂閱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ī)范
參考:
- https://www.cnblogs.com/rjzheng/p/9041659.html#!comments
- https://blog.csdn.net/z50L2O08e2u4afToR9A/article/details/81024553
- https://www.pianshen.com/article/1706860918/
- http://www.itdecent.cn/p/d00348a9eb3b
- https://www.cnblogs.com/qdhxhz/p/11046905.html
- https://www.cnblogs.com/rgcLOVEyaya/p/RGC_LOVE_YAYA_1003days.html
- https://github.com/liyue2008/canal-to-redis-example

