高并發(fā)下的緩存一致性,并發(fā),穿透問(wèn)題
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
76套java從入門到精通實(shí)戰(zhàn)課程分享
緩存在高并發(fā)場(chǎng)景下的常見(jiàn)問(wèn)題
緩存一致性問(wèn)題
當(dāng)數(shù)據(jù)時(shí)效性要求很高的時(shí)候,需要保證緩存中的數(shù)據(jù)與數(shù)據(jù)庫(kù)中的保持一致,而且需要保證緩存節(jié)點(diǎn)和副本中的數(shù)據(jù)也要保持一致,不能出現(xiàn)差異現(xiàn)象。這樣就比較依賴緩存的過(guò)期和更新策略。一般會(huì)在數(shù)據(jù)庫(kù)發(fā)生更改的時(shí)候,主動(dòng)更新緩存中的數(shù)據(jù)或者移除對(duì)應(yīng)的緩存。
1、更新數(shù)據(jù)庫(kù)成功—>更新緩存失敗—數(shù)據(jù)不一致
2、更新緩存成功—>更新數(shù)據(jù)庫(kù)失敗—數(shù)據(jù)不一致
3、更新數(shù)據(jù)庫(kù)成功—>淘汰緩存失敗—數(shù)據(jù)不一致
4、淘汰緩存成功—>更新數(shù)據(jù)庫(kù)失敗—查詢緩存丟失
緩存并發(fā)問(wèn)題
緩存過(guò)期后將嘗試從數(shù)據(jù)庫(kù)獲取數(shù)據(jù),在單線程情況下是合理而又穩(wěn)固的流程,但是在高并發(fā)情況下,有可能多個(gè)請(qǐng)求并發(fā)的從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù),對(duì)后端數(shù)據(jù)庫(kù)造成極大的壓力,甚至導(dǎo)致數(shù)據(jù)庫(kù)崩潰。另外,當(dāng)某個(gè)緩存的key被更新時(shí),同時(shí)也有可能在被大量的請(qǐng)求獲取,也同樣導(dǎo)致數(shù)據(jù)一致性的問(wèn)題。如何解決?一般我們會(huì)想到類似“鎖”的機(jī)制,在緩存更新或者過(guò)期的情況下,先獲取鎖,在進(jìn)行更新或者從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)后,再釋放鎖,需要一定的時(shí)間等待,就可以從緩存中繼續(xù)獲取數(shù)據(jù)
使用互斥鎖(mutex key)
只讓一個(gè)線程構(gòu)建緩存,其他線程構(gòu)建緩存的線程執(zhí)行完,重新從緩存獲取數(shù)據(jù)就ojbk了
單機(jī)直接用synchronized或者lock,分布式就用分布式鎖(可以用memcache的add,redis的setnx,zookeeper的節(jié)點(diǎn)添加監(jiān)聽(tīng)等等等…..)
memcache偽代碼如下
if (memcache.get(key) == null) {
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
} redis偽代碼
String get(String key){
String value = redis.get(key);
if(value == null){
if(redis.setnx(key_Mutex),"1"){
redis.expire(key_mutex,3*60);//防止死鎖
value = db.get(key);
redis.set(key,value);
resdis.delete(key_Mutex);
}else{
Thread.sleep(50);
get(key);
}
}
}
提前使用互斥鎖
在value內(nèi)部設(shè)置1個(gè)超時(shí)值(timeout1), timeout1比實(shí)際的memcache timeout(timeout2)小。當(dāng)從cache讀取到timeout1發(fā)現(xiàn)它已經(jīng)過(guò)期時(shí)候,馬上延長(zhǎng)timeout1并重新設(shè)置到cache。然后再?gòu)臄?shù)據(jù)庫(kù)加載數(shù)據(jù)并設(shè)置到cache中。偽代碼如下
v = memcache.get(key);
if (v == null) {
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
} else {
if (v.timeout <= now()) {
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
// extend the timeout for other threads
v.timeout += 3 * 60 * 1000;
memcache.set(key, v, KEY_TIMEOUT * 2);
// load the latest value from db
v = db.get(key);
v.timeout = KEY_TIMEOUT;
memcache.set(key, value, KEY_TIMEOUT * 2);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
}
}
上面兩種方案
優(yōu)點(diǎn):避免cache失效時(shí)刻大量請(qǐng)求獲取不到mutex并進(jìn)行sleep
缺點(diǎn):代碼復(fù)雜性增大,會(huì)出現(xiàn)死鎖和線程池阻塞等問(wèn)題,因此一般場(chǎng)合用方案一也已經(jīng)足夠
永遠(yuǎn)不過(guò)期
這里的“永遠(yuǎn)不過(guò)期”包含兩層意思:
(1) 從redis上看,沒(méi)有設(shè)置過(guò)期時(shí)間,就不會(huì)出現(xiàn)熱點(diǎn)key過(guò)期問(wèn)題,也就是“物理”不過(guò)期。
(2) 從功能上看,如果不過(guò)期,那不就成靜態(tài)的了嗎?所以我們把過(guò)期時(shí)間存在key對(duì)應(yīng)的value里,如果發(fā)現(xiàn)要過(guò)期了,通過(guò)一個(gè)后臺(tái)的異步線程進(jìn)行緩存的構(gòu)建,也就是“邏輯”過(guò)期,有一個(gè)問(wèn)題是在異步構(gòu)建緩存完成之前其他線程訪問(wèn)的是舊的數(shù)據(jù)
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
long timeout = v.getTimeout();
if (v.timeout <= System.currentTimeMillis()) {
// 異步更新后臺(tái)異常執(zhí)行
threadPool.execute(new Runnable() {
public void run() {
String keyMutex = "mutex:" + key;
if (redis.setnx(keyMutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(keyMutex, 3 * 60);
String dbValue = db.get(key);
redis.set(key, dbValue);
redis.delete(keyMutex);
}
}
});
}
return value;
} 資源保護(hù)(尚未了解)
緩存穿透問(wèn)題
場(chǎng)景:在高并發(fā)場(chǎng)景下,如果一個(gè)key被高并發(fā)訪問(wèn),沒(méi)有被命中,處于對(duì)容錯(cuò)性的考慮,會(huì)嘗試去從后端數(shù)據(jù)庫(kù)中獲取,從而導(dǎo)致了大量請(qǐng)求到達(dá)數(shù)據(jù)庫(kù),而當(dāng)該key對(duì)應(yīng)的數(shù)據(jù)本身就是空的情況下,就導(dǎo)致數(shù)據(jù)庫(kù)中并發(fā)地去執(zhí)行很多不必要的查詢操作,從而導(dǎo)致巨大沖擊和壓力
可以通過(guò)下面的幾種常用方式來(lái)避免緩存問(wèn)題
緩存空對(duì)象
對(duì)查詢結(jié)果為空的對(duì)象也進(jìn)行緩存,如果是集合,可以緩存一個(gè)空的集合(非null),如果是緩存單個(gè)對(duì)象,可以通過(guò)字段標(biāo)識(shí)來(lái)區(qū)分。這樣避免請(qǐng)求穿透到后端數(shù)據(jù)庫(kù),同時(shí),也需要保證緩存數(shù)據(jù)的時(shí)效性。適合命中不高,但可能被頻繁更新的數(shù)據(jù)

單獨(dú)過(guò)濾處理
對(duì)所有可能對(duì)應(yīng)數(shù)據(jù)為空的key進(jìn)行統(tǒng)一的存放,并在請(qǐng)求前做攔截,這樣避免請(qǐng)求穿透到后端數(shù)據(jù)庫(kù)。這種方式實(shí)現(xiàn)起來(lái)相對(duì)復(fù)雜,比較適合命中不高,但是更新不頻繁的數(shù)據(jù)
總結(jié):作為一個(gè)并發(fā)量較大的互聯(lián)網(wǎng)應(yīng)用,我們的目標(biāo)有3個(gè):
1. 加快用戶訪問(wèn)速度,提高用戶體驗(yàn)。
2. 降低后端負(fù)載,保證系統(tǒng)平穩(wěn)。
3. 保證數(shù)據(jù)“盡可能”及時(shí)更新(要不要完全一致,取決于業(yè)務(wù),而不是技術(shù)。)
---接下來(lái)一篇將對(duì) 緩存雪崩 做個(gè)簡(jiǎn)單的總結(jié)
————————————————
版權(quán)聲明:本文為CSDN博主「Kevins Danish」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。
原文鏈接:
https://blog.csdn.net/weixin_36708538/article/details/80338643
粉絲福利:Java從入門到入土學(xué)習(xí)路線圖
??????

??長(zhǎng)按上方微信二維碼 2 秒
感謝點(diǎn)贊支持下哈 
