一、緩存架構(gòu)
上圖是之前項目的緩存架構(gòu),加了二個級別的緩存:進(jìn)程內(nèi)緩存、分布式緩存。
讀操作的思路:判斷該緩存是否開啟了進(jìn)程內(nèi)緩存的開關(guān),開了則先讀進(jìn)程內(nèi)緩存,沒有則讀分布式緩存,若還是沒有就讀DB;
二、進(jìn)程內(nèi)緩存
2.1、進(jìn)程內(nèi)緩存的優(yōu)缺點
優(yōu)點:與分布式緩存相比較,因為緩存數(shù)據(jù)存儲在站點內(nèi),所以減少了一次網(wǎng)絡(luò)開銷。
缺點:分布式緩存雖然多了一次網(wǎng)絡(luò)開銷,但是數(shù)據(jù)仍然是統(tǒng)一存儲的,而進(jìn)程內(nèi)緩存的數(shù)據(jù)存了多份,這會導(dǎo)致數(shù)據(jù)修改時數(shù)據(jù)不一致性窗口比分布式緩存要長,一致性難保障。
2.2、如何解決進(jìn)程內(nèi)緩存的數(shù)據(jù)一致性問題?
(1)、將更改的通知寫進(jìn)MQ,讓所有站點訂閱,然后站點自己去DB讀取最新的數(shù)據(jù)
(2)、若是RPC框架,則用RPC框架的廣播功能通知所有站點,站點自己再去讀取DB
2.3、為什么為什么不能頻繁使用進(jìn)程內(nèi)緩存?
分層架構(gòu)設(shè)計,有一條準(zhǔn)則:站點層、服務(wù)層要做到無數(shù)據(jù)無狀態(tài),這樣才能任意的加節(jié)點水平擴(kuò)展,數(shù)據(jù)和狀態(tài)盡量存儲到后端的數(shù)據(jù)存儲服務(wù),例如數(shù)據(jù)庫服務(wù)或者緩存服務(wù)。
顯然進(jìn)程內(nèi)緩存違背了這一原則。
2.4、什么時候適合用進(jìn)程內(nèi)緩存?
只讀數(shù)據(jù),例如配置數(shù)據(jù),由于這些數(shù)據(jù)一般不會修改,所以可以考慮使用進(jìn)程內(nèi)緩存。
總結(jié):進(jìn)程內(nèi)緩存最大的問題是一致性難以保障,比較適合保存只讀數(shù)據(jù)。大部分場景使用進(jìn)程內(nèi)緩存的情況下,都可以改用分布式緩存。
三、分布式緩存
3.1、寫操作時,緩存應(yīng)該淘汰,還是修改?
(1)、修改緩存
a、可以減少一次cache miss,但是會加大業(yè)務(wù)處理的響應(yīng)
b、最嚴(yán)重的是,在寫并發(fā)時還可能會出現(xiàn)緩存數(shù)據(jù)錯誤
在1和2兩個并發(fā)寫發(fā)生時,由于無法保證時序,此時不管先操作緩存還是先操作數(shù)據(jù)庫,都可能出現(xiàn):
i、請求1先操作數(shù)據(jù)庫,請求2后操作數(shù)據(jù)庫
ii、請求2先set了緩存,請求1后set了緩存
導(dǎo)致,數(shù)據(jù)庫與緩存之間的數(shù)據(jù)不一致。
(2)、淘汰緩存
會讓讀數(shù)據(jù)時多了一次cache miss。
總結(jié):淘汰緩存。一般的情況下讀數(shù)據(jù)時多一次cache miss并不會有問題,僅僅是加大了響應(yīng)時間,由于硬件水品的提高,一般情況下感受不到。
3.2、寫操作時,先操作數(shù)據(jù)庫,還是先操作緩存?
希望保證兩個操作的原子性,要么同時成功,要么同時失敗。
這演變?yōu)橐粋€分布式事務(wù)的問題,保證原子性十分困難,很有可能出現(xiàn)一半成功,一半失敗,接下來看下,當(dāng)原子性被破壞的時候,分別會發(fā)生什么。
(1)、先淘汰緩存、后操作數(shù)據(jù)庫
第1步失敗,第2步成功,會導(dǎo)致,緩存里的還是舊數(shù)據(jù),數(shù)據(jù)庫的是新數(shù)據(jù),業(yè)務(wù)無法接受。若第1步成功,第2步失敗,會導(dǎo)致緩存清空了,數(shù)據(jù)庫還是舊數(shù)據(jù),也只是會多一次cache miss而已,試想一下這里是更新緩存會有什么后果,所以這也是為什么建議淘汰緩存而不是修改緩存的原因之一。
(2)、先操作數(shù)據(jù)庫、后淘汰緩存
第1步成功,第2步失敗,會導(dǎo)致,數(shù)據(jù)庫里是新數(shù)據(jù),而緩存里是舊數(shù)據(jù),業(yè)務(wù)無法接受。若第1步失敗就可以回滾,不會出現(xiàn)數(shù)據(jù)不一致。
因為redis操作和mysql操作不在一個事務(wù)里面,所以保證原子性較麻煩,有人可能想到用分布式事務(wù),但是大可不必。可以采用一方方法,即當(dāng)redis操作失敗時,手動拋出運行時異常。
要注意的是先淘汰緩存,后操作數(shù)據(jù)庫,還可能會導(dǎo)致如下坑:
當(dāng)讀寫并發(fā)時會出問題。淘汰緩存后,數(shù)據(jù)庫更新事務(wù)提交前,若有讀操作,則會從數(shù)據(jù)庫中讀取舊數(shù)據(jù)存到緩存,之后數(shù)據(jù)事務(wù)提交,可是舊數(shù)據(jù)會一直存放在緩存中。
總結(jié):先操作數(shù)據(jù)庫,后淘汰緩存
3.3、DB主從架構(gòu)下導(dǎo)致的緩存與DB數(shù)據(jù)不一致性
(1)、為什么出現(xiàn)不一致性
主從同步有延時,在延期的期間,發(fā)生cache miss后讀取了從庫,就會導(dǎo)致數(shù)據(jù)不一致性了。本質(zhì)就是數(shù)據(jù)庫主從同步延時。
(2)、該情況的數(shù)據(jù)不一致性不良影響
沒有緩存架構(gòu)下時,數(shù)據(jù)庫主從延時,只會導(dǎo)致同步期間短暫的數(shù)據(jù)不一致性,當(dāng)同步完成后,不一致性不存在。而有了緩存后,緩存寫進(jìn)了不一致性的從庫數(shù)據(jù),當(dāng)數(shù)據(jù)庫同步完成后數(shù)據(jù)不一致性一直存在,直到緩存過期。可見加入緩存后,這個問題不出力妥當(dāng),影響會很糟糕。能不能使得加入緩存后,數(shù)據(jù)不一致性的時間與每加入緩存相當(dāng)呢?
(3)、解決辦法
a、主從同步;
b、通過工具訂閱從庫的binlog,這里能夠最準(zhǔn)確的知道,從庫數(shù)據(jù)同步完成的時間。訂閱工具是DTS,可以是cannal,也可以自己訂閱和分析binlog;
c、從庫執(zhí)行完寫操作,向緩存再次發(fā)起刪除,淘汰這段時間內(nèi)可能寫入緩存的舊數(shù)據(jù);
總結(jié):該辦法也只能優(yōu)化,并不能消除數(shù)據(jù)不一致性。
3.4、主從數(shù)據(jù)庫不一致解決方法
主從數(shù)據(jù)庫不一致性的根本原因就是主從數(shù)據(jù)庫同步延遲。
解決方法如下
(1)、忽略
業(yè)務(wù)允許范圍下,可以忽略這種段時間內(nèi)的數(shù)據(jù)不一致性。
(2)、強制讀主
搭建高可用主庫,讀寫都在主庫,采用緩存來提高讀性能并且達(dá)到減輕數(shù)據(jù)庫壓力的目的。
(3)、選擇性讀主
強制讀主顯得過于粗暴,因為只有少數(shù)的寫才會導(dǎo)致這種數(shù)據(jù)不一致性。
可以這樣做:
a、寫主庫;
b、將哪個庫,哪個表,哪個主鍵三個信息拼裝一個key設(shè)置到cache里,這條記錄的超時時間,設(shè)置為“主從同步時延”;要注意的是,?假設(shè)主從延時為1s,這個key的cache超時時間也為1s。
c、讀操作時,把哪個庫,哪個表,哪個主鍵這三個信息拼裝一個key,到cache里去查詢,如果有的話就讀主,沒有就讀從。
四、緩存使用總結(jié)
只要有數(shù)據(jù)冗余的地方,就會有數(shù)據(jù)不一致性問題。緩存的難點,從我接觸的來看,有兩點:緩存不一致性、緩存數(shù)據(jù)不存在。而緩存數(shù)據(jù)不存在導(dǎo)致的問題可以分為3類:緩存穿透、緩存擊穿、緩存雪崩。從使用的感受來看,緩存的坑還挺多的,使用時要根據(jù)業(yè)務(wù)來選擇方案,只有符合業(yè)務(wù)的才是最好的。