緩存和數據庫一致性問題
本文討論的背景是,cache如memcache,redia等緩存來緩存數據庫讀取出來的數據,以提高讀性能,如何處理緩存里的數據和數據庫數據的一致性是本文討論的內容:正常的緩存步驟是:
1查詢緩存數據是否存在,2不存在即查詢數據庫,3將數據添加到緩存同時返回結果,4下一次訪問發現緩存存在即直接返回緩存數據。那么當更新數據庫數據的時候,該如果更新緩存呢,至少要考慮盡量短時間的一致性,這個看業務需求,比如用戶信息緩存時間越短越好,比如排行榜可能是一天更新一次,本文純技術討論,就是盡量縮短非一致性的時間以此來學習思路。
1、當更新數據庫時候,緩存應該如何更新
1.1、更新緩存VS淘汰緩存
答:更新緩存很直接,但是涉及到本次更新的數據結果需要一堆數據運算(例如更新用戶余額,可能需要先看看有沒有優惠券等),復雜度就增加了。而淘汰緩存僅僅會增加一次cache miss,代價可以忽略,所以建議淘汰緩存
1.2、先淘汰后寫數據庫vs先寫數據庫后淘汰
答: 先寫后淘汰,如果淘汰失敗,cache里一直是臟數據
先淘汰后寫,下次請求的時候緩存就會miss hit一次,這個代價是可以忽略的,(如果淘汰失敗return false)
綜合比較統,推薦先淘汰緩存再寫數據庫
下次請求直接從數據庫取然后再寫在緩存里。(當然這里可能會大並發一起擊穿(通過下面1.3的方式可以解決),還有在淘汰緩存再寫數據庫的這一瞬間,再來一個讀取請求,這個讀取比上一個請求的寫先完成,那么就會出現臟數據。網上有人說 修改數據庫的連接池方法,就是對於同一個ID的數據請求,比如query(id),edit(id)都使用同一個連接對象,這樣來保證先來的先完成,貌似還是挺復雜的,關於后來的讀取請求先與先來的寫請求完成,只能通過這樣的串行方式執行)
關於臟數據,如果需要強一致性
1.2.1、可以通過數據庫無論是讀或寫操作都是通過一個請求db connection連接完成(目的是串行),這樣就需要修改連接池
1.2.2、可以采用更新緩存而不是淘汰緩存,前提是更新的代價比較低
1.2.3、可以先更新數據庫再淘汰緩存(更新的原則是誰影響小先更新誰,此處倒着來了,一般推薦先更新數據庫再淘汰緩 存),不過一般情況,淘汰緩存失敗的可能性很小,可以以緩存處理100%不失敗為前期。
1.2.4、雙淘汰發,即:淘汰緩存-更新數據庫-淘汰緩存,可以盡量減少臟數據的留存時間。
1.2.5、以上實現起來,要么極短時間的不一致要么一致性代價比較高,實際項目我會這樣處理,更新數據庫的地方和讀取的地方上同樣key的分布式鎖,這樣就能保證,先操作(或讀或寫)數據的先獲得結果,實際中這樣的強一致需求比較少,參考思路即可。
當然數據既然都緩存起來了,絕大部分都不要求強一致性,為了盡可能的縮短一致性的時間,可以如下處理:
1.2.6、異步消息總線esb更新法,即:修改數據庫往消息總線里發送一個消息,在接收端去處理這個消息更新緩存,缺點是有代碼入侵
1.2.7,異步binlog掃描更新法,增量的去掃描binlog中的修改記錄,符合條件的更新緩存,相比消息總線法沒有代碼入侵
1.3、在1.2緩存miss hit的時候,此時大並發請求這個過程,會出現什么異常
答:緩存擊穿(關於緩存丟失導致雪崩擊穿參考:https://blog.csdn.net/zeb_perfect/article/details/54135506)
主要是熱點key的請求或者一直沒寫緩存成功會出現這種情況,解決方案網上很多,這里我寫下的我解決方案:
偽代碼:
//主要是數據庫查詢的時候串行,會帶來毫秒級的卡頓,綜合復雜度性能等,推薦此方法
String json="";
cache=redis.get(key);
if(cache is not null)
{
return cache
}
}
else
{
lock();//如果分布式部署,這里有用分布式鎖哦,分布式鎖來鎖住數據庫查詢請求,應盡量避免鎖,這樣程序就是單線程達不到並發要求,這里使用鎖主要是因 極少概率會穿透到數據庫,鎖一點點時間不影響性能
//為什么再來一次判斷,自行想象下高並發場景下
cache=redis.get(key);
if(cache is not null)
{
return cache;
}
data=json=server.Query(Sql);
redis.set(data,key);
unlock();
return data;
}
分布式鎖:
//分布式鎖
String get(String key) {
String value = redis.get(key);
if (value == null) {
if (redis.setnx(key_mutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(key_mutex, 3 * 60)
value = db.get(key);
redis.set(key, value);
redis.delete(key_mutex);
} else {
//其他線程休息50毫秒后重試
Thread.sleep(50);
get(key);
}
}
}
備注:設計緩存的時候,尤其是熱點key過期的時候 需要考慮擊穿,以及雪崩,穿透等情形對下游DB的並發請求帶來的影響
https://blog.csdn.net/zeb_perfect/article/details/54135506
https://blog.csdn.net/wang0112233/article/details/79558612
2、1中的方案都是在單主庫的環境下討論的,如果涉及到主從數據庫如何處理呢?
一般主從都是 寫主讀從,寫主后,立馬讀從,而從還沒有更新,有一定的延遲,這個延遲時間我們經驗總結暫定500ms,超過500ms超時返回。如果主從的話涉及到強一致性更復雜,這里暫且按照弱一致性的需求,只是要盡量的縮短非一致性的時間
2.1、淘汰緩存,修改完數據,thread.wait(500s),再次淘汰緩存,下次讀從庫就是最新數據,在期間有可能500ms的舊數據
2.2、1.2.6和1.2.7一樣,只是在處理更新緩存的時候加上500ms的延遲時間,以此來保證從庫更新完成,再更新緩存
3、主從一致性,即修改完立馬就要讀取到最新的數據(本方案不涉及到緩存的同步,如果涉及可以結合全篇思路去設計) 方案如下:
3.1、半同步復制,理應數據庫原生的功能,等從庫同步完才返回結果,缺點吞吐量下降
3.2、強制讀主庫,部分有一致性要求的,代碼中強制讀取主庫,這個時候一定要結合好緩存,提高讀性能
3.3、數據庫中間件,一般情況數據庫中間件把寫路由到主,把讀路由到從,此處是記錄所以寫的key,在500ms內讀主庫,超過500ms后讀從庫,能保證絕對的一致性,缺點是成本比較高
3.4、緩存記錄寫key法,發生寫操作,把此key記錄在緩存里過期時間500ms,key存在表示剛更新過,還沒完成同步,強制路由到主庫,沒有則路由到從庫
關於強一致的需求,現實是不多的,本身就使用cache了還要求強一致,貌似本末倒置,但是不排除特殊情況的存在,主要是思路和大家分享。
--------------------- 作者:zlhzhj 來源:CSDN 原文:https://blog.csdn.net/ZLHZHJ/article/details/80176988?utm_source=copy 版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
