緩存是一種提高系統(tǒng)讀性能的常見技術(shù),對于讀多寫少的應(yīng)用場景,我們經(jīng)常使用緩存來進(jìn)行優(yōu)化。無論使用哪一種緩存技術(shù),有些問題是緩存都要面臨的??偨Y(jié)歸納一下。
名詞解釋:
緩存的命中率= 命中緩存請求個數(shù)/總緩存訪問請求個數(shù) = hit/(hit+miss)
緩存穿透
問題描述:
我們在項目中使用緩存通常都是先檢查緩存中是否存在,如果存在直接返回緩存內(nèi)容,如果不存在就直接查詢數(shù)據(jù)庫然后再緩存查詢結(jié)果返回。這個時候如果我們查詢的某一個數(shù)據(jù)在緩存中一直不存在,就會造成每一次請求都查詢DB,這樣緩存就失去了意義,在流量大時,可能DB就掛掉了。
解決思路:
將這個不存在的key預(yù)先設(shè)定一個值。比如,"key" , “&&”。
在返回這個&&值的時候,我們的應(yīng)用就可以認(rèn)為這是不存在的key,那我們的應(yīng)用就可以決定是否繼續(xù)等待繼續(xù)訪問,還是放棄掉這次操作。如果繼續(xù)等待訪問,過一個時間輪詢點后,再次請求這個key,如果取到的值不再是&&,則可以認(rèn)為這時候key有值了,從而避免了透傳到數(shù)據(jù)庫,從而把大量的類似請求擋在了緩存之中。
緩存并發(fā)
問題描述:
有時候如果網(wǎng)站并發(fā)訪問高,一個緩存如果失效,可能出現(xiàn)多個進(jìn)程同時查詢DB,同時設(shè)置緩存的情況,如果并發(fā)確實很大,這也可能造成DB壓力過大。
解決思路:
【1】我們會想到類似“鎖”的機(jī)制,在緩存更新或者過期的情況下,先嘗試獲取到鎖,當(dāng)更新或者從數(shù)據(jù)庫獲取完成后再釋放鎖,其他的請求只需要犧牲一定的等待時間,即可直接從緩存中繼續(xù)獲取數(shù)據(jù)。
這個方法不靠譜,性能會很差。分布式情況下,如果只是單機(jī)使用鎖,還是會有DB并發(fā)問題
【2】定期從DB里查詢數(shù)據(jù),再刷到緩存里面,確保緩存里面的數(shù)據(jù)一直可以讀到。
這種方法有個缺點是,有些業(yè)務(wù)的key可能是變化的,不確定的。
【3】兩個key,一個key用來存放數(shù)據(jù),另一個用來標(biāo)記失效時間。
比如key是aaa,設(shè)置失效時間為30s,則另一個key為expire_aaa,失效時間為25s。在取數(shù)據(jù)時,同時取出aaa和expire_aaa,如果expire_aaa的value == null,則后臺啟動一個任務(wù)去查詢DB,更新緩存。對于后臺啟動一個任務(wù)去查詢DB,更新緩存,要保證一個key只有一個線程在執(zhí)行,這個如何實現(xiàn)?
對于同一個進(jìn)程,簡單加鎖即可。拿到鎖的就去更新DB,沒拿到鎖的直接返回。
對于集群式的部署的,如何實現(xiàn)只允許一個任務(wù)執(zhí)行?可以使用分布式鎖。
當(dāng)get expired_aaa是null時,則add expired_aaa 過期時間由自己靈活處理。比如設(shè)置為3秒。
如果成功了,再去查詢DB,查到數(shù)據(jù)后,再set expired_aaa為25秒。set aaa 為30秒。
綜上所述,來梳理下流程:
比如一個key是aaa,失效時間是30s。查詢DB在1s內(nèi)。
put數(shù)據(jù)時,設(shè)置aaa過期時間30s,設(shè)置expire_aaa過期時間25s;
get數(shù)據(jù)時,multiget aaa 和 expire_aaa,如果expired_aaa對應(yīng)的value != null,則直接返回aaa對應(yīng)的數(shù)據(jù)給用戶。如果expire_aaa返回value == null,則后臺啟動一個任務(wù),嘗試add expire_aaa,并設(shè)置超時過間為3s。這里設(shè)置為3s是為了防止后臺任務(wù)失敗或者阻塞,如果這個任務(wù)執(zhí)行失敗,那么3秒后,如果有另外的用戶訪問,那么可以再次嘗試查詢DB。如果add執(zhí)行成功,則查詢DB,再更新aaa的緩存,并設(shè)置expire_aaa的超時時間為25s。
這種方法的好處是從緩存里面查詢數(shù)據(jù)的線程不受阻塞,同時查詢db的線程不會多,即使在分布式不使用分布式鎖,至多線程數(shù)也就是機(jī)器數(shù)
緩存失效
問題描述:
引起這個問題的主要原因還是高并發(fā)的時候,平時我們設(shè)定一個緩存的過期時間時,可能有一些會設(shè)置1分鐘啊,5分鐘這些,并發(fā)很高時可能會出在某一個時間同時生成了很多的緩存,并且過期時間都一樣,這個時候就可能引發(fā)一當(dāng)過期時間到后,這些緩存同時失效,請求全部轉(zhuǎn)發(fā)到DB,DB可能會壓力過重。
解決思路:
將緩存失效時間分散開,比如我們可以在原有的失效時間基礎(chǔ)上增加一個隨機(jī)值,比如1-5分鐘隨機(jī),這樣每一個緩存的過期時間的重復(fù)率就會降低,就很難引發(fā)集體失效的事件。
更新緩存 VS 淘汰緩存
什么是更新緩存:數(shù)據(jù)不但寫入數(shù)據(jù)庫,還會寫入緩存
什么是淘汰緩存:數(shù)據(jù)只會寫入數(shù)據(jù)庫,不會寫入緩存,只會把數(shù)據(jù)淘汰掉
更新緩存的優(yōu)點:緩存不會增加一次miss,命中率高
淘汰緩存的優(yōu)點:簡單
先操作數(shù)據(jù)庫 vs 先操作緩存
假設(shè)先寫數(shù)據(jù)庫,再淘汰緩存:第一步寫數(shù)據(jù)庫操作成功,第二步淘汰緩存失敗,則會出現(xiàn)DB中是新數(shù)據(jù),Cache中是舊數(shù)據(jù),數(shù)據(jù)不一致。
假設(shè)先淘汰緩存,再寫數(shù)據(jù)庫:第一步淘汰緩存成功,第二步寫數(shù)據(jù)庫失敗,則只會引發(fā)一次Cache miss。
所以結(jié)論是:先淘汰緩存,再寫數(shù)據(jù)庫