1. 緩存優(yōu)缺點(diǎn)
緩存常用的結(jié)構(gòu)如下:
1.1. 優(yōu)點(diǎn)
加速讀寫:由于數(shù)據(jù)庫讀寫速度慢,而基于內(nèi)存的讀寫速度快,所以使用緩存可以加速讀寫想幻,優(yōu)化用戶體驗(yàn)
降低后端負(fù)載:幫助后端減少訪問量和復(fù)雜計(jì)算五辽,從而降低了后端的負(fù)載。
1.2 缺點(diǎn)
數(shù)據(jù)不一致:緩存層和存儲(chǔ)層可能存在數(shù)據(jù)不一致的問題诚亚,具體何時(shí)一致和同步更新策略有關(guān)。
代碼維護(hù)成本增加:要同時(shí)維護(hù)緩存層和存儲(chǔ)層
運(yùn)維成本增加
2. 緩存更新策略
2.1 LRU/LFU/FIFO算法剔除
剔除算法通常用于緩存超過預(yù)設(shè)的最大值的時(shí)候午乓,如何對(duì)現(xiàn)有的數(shù)據(jù)進(jìn)行剔除站宗。redis使用maxmemory-policy這個(gè)配置作為內(nèi)存超過預(yù)設(shè)的最大值后對(duì)于數(shù)據(jù)的剔除策略。
2.2 超時(shí)剔除
通過給緩存數(shù)據(jù)設(shè)置過期時(shí)間益愈,讓其在過期時(shí)間后自動(dòng)刪除梢灭,如expire命令。
2.3 主動(dòng)更新
真實(shí)數(shù)據(jù)更新后,立即更新緩存數(shù)據(jù)敏释。
3. 緩存粒度控制
以使用redis+mysql為例:
首先從數(shù)據(jù)庫中獲取用戶信息:
select * from user where id={id}
然后再將數(shù)據(jù)保存到redis中:
set user 'select * from user where id={id}'
這樣表中的全部列的信息都保存到緩存中了库快,但是如果想保存部分列呢:
set user 'select id,name... from user where id={id}'
如果列很多的時(shí)候,這樣一一列舉就不太方便颂暇,導(dǎo)致代碼可維護(hù)性增加缺谴。
緩存全部和緩存部分對(duì)比如下,使用時(shí)要自行取舍:
- 緩存全部:通用性高耳鸯,占用空間大湿蛔,代碼維護(hù)簡單
- 緩存部分:通用性低,占用空間小县爬,代碼維護(hù)復(fù)雜
4. 緩存穿透
通常情況下阳啥,處于容錯(cuò)的考慮,根據(jù)key先去緩存層查詢财喳,如果緩存查不到察迟,再去數(shù)據(jù)查詢。如果數(shù)據(jù)庫也查不到數(shù)據(jù)則不寫入緩存層耳高。圖示如下:
如果一些惡意攻擊對(duì)很多此類緩存和存儲(chǔ)層都沒有的值進(jìn)行查詢扎瓶,就會(huì)導(dǎo)致緩存層沒有起到保護(hù)存儲(chǔ)層的效果,大量請(qǐng)求加大數(shù)據(jù)庫負(fù)載泌枪,從而導(dǎo)致宕機(jī)概荷,這就是緩存穿透的后果。
4.1 優(yōu)化
為解決上述緩存穿透問題碌燕,可以使用下述方法進(jìn)行優(yōu)化误证。
4.1.1 緩存空對(duì)象
當(dāng)數(shù)據(jù)庫查不到數(shù)據(jù)時(shí),仍將空對(duì)象保存到緩存中修壕。之后再訪問這個(gè)數(shù)據(jù)時(shí)愈捅,就會(huì)先去訪問緩存,這樣就起到了保護(hù)數(shù)據(jù)庫的作用慈鸠。
緩存空對(duì)象有如下問題:
- 如果這些空對(duì)象很多的時(shí)候蓝谨,也會(huì)占用過多的redis存儲(chǔ)空間,導(dǎo)致緩存的壓力加大青团,比較有效的方法是像棘,設(shè)置一個(gè)較短的過期時(shí)間,讓其自動(dòng)剔除壶冒。
4.1.2 布隆過濾器攔截
將可能出現(xiàn)的緩存key的組合方式的所有數(shù)值以hash形式存儲(chǔ)在一個(gè)很大的bitmap中<布隆過濾器>(需要考慮如何將這個(gè)可能出現(xiàn)的數(shù)據(jù)的hash值之后同步到bitmap中, eg. 后端每次新增一個(gè)可能的組合就同步一次)截歉,一個(gè)一定不存在的數(shù)據(jù)會(huì)被這個(gè)bitmap攔截掉胖腾,從而避免了對(duì)底層存儲(chǔ)系統(tǒng)的查詢壓力。
布隆過濾器類似于將一個(gè)key通過n個(gè)不同的hash函數(shù)定位成n個(gè)整數(shù),然后將這n個(gè)整數(shù)定位在一個(gè)長度在M的初始數(shù)值為0的數(shù)組下標(biāo)上咸作,設(shè)置該n個(gè)下標(biāo)的數(shù)值為1锨阿。 那只要當(dāng)查詢過來,用這n個(gè)hash函數(shù)定位判定都為1那基本就存在记罚,只要有任一下標(biāo)的數(shù)組值不是1墅诡,則代表不存在。
5. 雪崩優(yōu)化
下面描述了什么是緩存雪崩:由于緩存層承載著大量請(qǐng)求桐智,有效地保護(hù)了存儲(chǔ)層末早,但是如果緩存層由于某些原因不能提供服務(wù),于是所有的請(qǐng)求都會(huì)達(dá)到存儲(chǔ)層说庭,存儲(chǔ)層的調(diào)用量會(huì)暴增然磷,造成存儲(chǔ)層也會(huì)級(jí)聯(lián)宕機(jī)的情況。緩存雪崩的英文原意是stampeding herd(奔逃的野牛)刊驴,指的是緩存層宕掉后姿搜,流量會(huì)像奔逃的野牛一樣,打向后端存儲(chǔ)捆憎。
預(yù)防和解決緩存雪崩問題舅柜,可以從以下三個(gè)方面進(jìn)行著手。
1)保證緩存層服務(wù)高可用性 躲惰。和飛機(jī)都有多個(gè)引擎一樣致份,如果緩存層設(shè)計(jì)成高可用的,即使個(gè)別節(jié)點(diǎn)礁扮、個(gè)別機(jī)器知举、甚至是機(jī)房宕掉,依然可以提供服務(wù)太伊,例如前面介紹過的Redis Sentinel和Redis Cluster都實(shí)現(xiàn)了高可用雇锡。
2)依賴隔離組件為后端限流并降級(jí) 。無論是緩存層還是存儲(chǔ)層都會(huì)有出錯(cuò)的概率僚焦,可以將它們視同為資源锰提。作為并發(fā)量較大的系統(tǒng),假如有一個(gè)資源不可用芳悲,可能會(huì)造成線程全部阻塞(hang)在這個(gè)資源上立肘,造成整個(gè)系統(tǒng)不可用。降級(jí)機(jī)制在高并發(fā)系統(tǒng)中是非常普遍的名扛,如Java依賴隔離工具Hystrix谅年。
3)提前演練 。在項(xiàng)目上線前肮韧,演練緩存層宕掉后融蹂,應(yīng)用以及后端的負(fù)載情況以及可能出現(xiàn)的問題旺订,在此基礎(chǔ)上做一些預(yù)案設(shè)定。
6. 緩存擊穿
開發(fā)人員通常使用“緩存+過期時(shí)間”的策略既可以加速數(shù)據(jù)讀寫超燃,又保證數(shù)據(jù)的定期更新区拳,這種模式基本能夠滿足絕大部分需求。但是如果當(dāng)前key是一個(gè)熱點(diǎn)key(例如一個(gè)熱門的娛樂新聞)意乓,并發(fā)量非常大樱调,那這個(gè)key正好到了過期時(shí)間失效了,導(dǎo)致眾多請(qǐng)求都獲取不到這個(gè)原保存到緩存中的key届良,從而全部去請(qǐng)求數(shù)據(jù)庫了笆凌,但是執(zhí)行數(shù)據(jù)庫可能是一個(gè)復(fù)雜計(jì)算,例如復(fù)雜的SQL伙窃、多次IO菩颖、多個(gè)依賴等,就會(huì)瞬間增大數(shù)據(jù)庫的壓力为障,引起數(shù)據(jù)庫服務(wù)器宕機(jī)晦闰,這就是緩存擊穿。
解決方案如下鳍怨。
6.1 互斥鎖
當(dāng)緩存中沒有數(shù)據(jù)的時(shí)候會(huì)去訪問數(shù)據(jù)并重寫到緩存呻右,這個(gè)過程可稱其為重建緩存。
此方法只允許一個(gè)線程重建緩存鞋喇,其他線程等待重建緩存的線程執(zhí)行完声滥,重新從緩存獲取數(shù)據(jù)即可,示例代碼如下:
String get(String key){
String value = redis.get(key);
if(value == null){
String mutexKey = "mutex:key:"+key; // 設(shè)置作為上鎖的key
if(redis.set(mutexKey,"1","ex 180","nx")){ // 使用setnx上鎖
value = db.get(key); // 從數(shù)據(jù)庫中獲取
redis.setex(key,timeout,value); // 重建該key的緩存
redis.delete(mutexKey); // 解鎖
} else {
Thread.sleep(50);
get(key);
}
}
return value;
}
1)從Redis獲取數(shù)據(jù)侦香,如果值不為空落塑,則直接返回值;否則執(zhí)行下面的2.1)和2.2)步驟罐韩。
2.1)如果set(nx和ex)結(jié)果為true憾赁,說明此時(shí)沒有其他線程重建緩存,那么當(dāng)前線程執(zhí)行緩存構(gòu)建邏輯散吵。
2.2)如果set(nx和ex)結(jié)果為false龙考,說明此時(shí)已經(jīng)有其他線程正在執(zhí)行構(gòu)建緩存的工作,那么當(dāng)前線程將休息指定時(shí)間(這里為50毫秒矾睦,取決于構(gòu)建緩存的速度)后晦款,重新執(zhí)行函數(shù),直到獲取到數(shù)據(jù)枚冗。
使用互斥鎖存在的問題就是通過加鎖阻塞其他調(diào)用方的方式缓溅,可能會(huì)存在死鎖和線程阻塞的風(fēng)險(xiǎn)。
6.2 永遠(yuǎn)不過期
“永遠(yuǎn)不過期”包含兩層意思:
從緩存層面來看赁温,確實(shí)沒有設(shè)置過期時(shí)間肛宋,所以不會(huì)出現(xiàn)熱點(diǎn)key過期后產(chǎn)生的問題州藕,也就是“物理”不過期。
-
從功能層面來看酝陈,為每個(gè)value設(shè)置一個(gè)邏輯過期時(shí)間,當(dāng)發(fā)現(xiàn)超過邏輯過期時(shí)間后毁涉,會(huì)使用單獨(dú)的線程去構(gòu)建緩存(相當(dāng)于不使用redis的過期功能沉帮,而是自己實(shí)現(xiàn)過期判斷邏輯)。
示例代碼如下:
String get(final String key){ V v = redis.get(key); String value = v.getValue(); long logicTimeout = v.getLogicTimeout();// 自定義邏輯超時(shí)時(shí)間 if(logicTimeout<=System.currentTimeMillis()){ String mutexKey = "mutex:key:"+key; // 設(shè)置作為上鎖的key if(redis.set(mutexKey,"1","ex 180","nx")){ // 使用setnx上鎖 threadPool.execute(new Runnable(){ public void run(){ String dbValue = db.get(key); // 從數(shù)據(jù)庫中獲取 redis.set(key,(dbValue,newLogicTimeout)); // 重建該key的緩存 redis.delete(mutexKey); // 解鎖 } }); } } }
上述實(shí)現(xiàn)有一個(gè)問題就是在單獨(dú)創(chuàng)建線程重新構(gòu)建緩存的過程中贫堰,如果有其他服務(wù)去獲取緩存中的該值就會(huì)取到舊值穆壕,即出現(xiàn)短暫數(shù)據(jù)不一致的問題。