參考:
如何保證緩存與數(shù)據(jù)庫的雙寫一致性条舔?
一般來說枫耳,如果允許緩存可以稍微的跟數(shù)據(jù)庫偶爾有不一致的情況,也就是說如果你的系統(tǒng)不是嚴格要求 “緩存+數(shù)據(jù)庫” 必須保持一致性的話孟抗,最好不要做這個方案迁杨,即:讀請求和寫請求串行化,串到一個內(nèi)存隊列里去凄硼。
串行化可以保證一定不會出現(xiàn)不一致的情況铅协,但是它也會導致系統(tǒng)的吞吐量大幅度降低,用比正常情況下多幾倍的機器去支撐線上的一個請求摊沉。
1.Cache Aside Pattern
最經(jīng)典的緩存+數(shù)據(jù)庫讀寫的模式狐史,就是 Cache Aside Pattern。
- 讀的時候说墨,先讀緩存骏全,緩存沒有的話,就讀數(shù)據(jù)庫尼斧,然后取出數(shù)據(jù)后放入緩存姜贡,同時返回響應。
- 更新的時候棺棵,先更新數(shù)據(jù)庫鲁豪,然后再刪除緩存潘悼。
A.為什么是刪除緩存,而不是更新緩存爬橡?
原因很簡單治唤,很多時候,在復雜點的緩存場景糙申,緩存不單單是數(shù)據(jù)庫中直接取出來的值宾添。比如可能更新了某個表的一個字段,然后其對應的緩存柜裸,是需要查詢另外兩個表的數(shù)據(jù)并進行運算缕陕,才能計算出緩存最新的值的。
另外更新緩存的代價有時候是很高的疙挺。每次修改數(shù)據(jù)庫的時候扛邑,都一定要將其對應的緩存更新一份?這個緩存到底會不會被頻繁訪問到铐然?
其實刪除緩存蔬崩,而不是更新緩存,就是一個 lazy 計算的思想搀暑,不要每次都重新做復雜的計算沥阳,不管它會不會用到,而是讓它到需要被使用的時候再重新計算自点。像 mybatis桐罕,hibernate,都有懶加載思想桂敛。
2.最初級的緩存不一致問題及解決方案
先更新數(shù)據(jù)庫功炮,再刪除緩存。如果刪除緩存失敗了术唬,那么會導致數(shù)據(jù)庫中是新數(shù)據(jù)薪伏,緩存中是舊數(shù)據(jù),數(shù)據(jù)就出現(xiàn)了不一致碴开。
解決思路:
先刪除緩存毅该,再更新數(shù)據(jù)庫博秫。如果數(shù)據(jù)庫更新失敗了潦牛,那么數(shù)據(jù)庫中是舊數(shù)據(jù),緩存中是空的挡育,那么數(shù)據(jù)不會不一致巴碗。
因為讀的時候緩存沒有,所以去讀了數(shù)據(jù)庫中的舊數(shù)據(jù)即寒,然后更新到緩存中橡淆。
3.比較復雜的數(shù)據(jù)不一致問題分析
數(shù)據(jù)發(fā)生了變更召噩,先刪除了緩存,然后要去修改數(shù)據(jù)庫逸爵,此時還沒修改具滴。一個請求過來,去讀緩存师倔,發(fā)現(xiàn)緩存空了构韵,去查詢數(shù)據(jù)庫,查到了修改前的舊數(shù)據(jù)趋艘,放到了緩存中疲恢。隨后數(shù)據(jù)變更的程序完成了數(shù)據(jù)庫的修改。完了瓷胧,數(shù)據(jù)庫和緩存中的數(shù)據(jù)不一樣了...
A.為什么上億流量高并發(fā)場景下显拳,緩存會出現(xiàn)這個問題?
只有在對一個數(shù)據(jù)在并發(fā)的進行讀寫的時候搓萧,才可能會出現(xiàn)這種問題杂数。如果并發(fā)量很低的情況下,不會出現(xiàn)剛才描述的那種不一致的場景矛绘。
如果每天的是上億的流量耍休,每秒并發(fā)讀是幾萬,每秒只要有數(shù)據(jù)更新的請求货矮,就可能會出現(xiàn)上述的數(shù)據(jù)庫+緩存不一致的情況羊精。
B.解決方案如下:
更新數(shù)據(jù)的時候,根據(jù)數(shù)據(jù)的唯一標識囚玫,將操作路由之后喧锦,發(fā)送到一個 jvm 內(nèi)部隊列中。讀取數(shù)據(jù)的時候抓督,如果發(fā)現(xiàn)數(shù)據(jù)不在緩存中燃少,那么將重新執(zhí)行“讀取數(shù)據(jù)+更新緩存”的操作,根據(jù)唯一標識路由之后铃在,也發(fā)送到同一個 jvm 內(nèi)部隊列中阵具。
一個隊列對應一個工作線程,每個工作線程串行拿到對應的操作定铜,然后一條一條的執(zhí)行阳液。這樣的話,一個數(shù)據(jù)變更的操作揣炕,先刪除緩存帘皿,然后再去更新數(shù)據(jù)庫,但是還沒完成更新畸陡。此時如果一個讀請求過來鹰溜,沒有讀到緩存虽填,那么可以先將緩存更新的請求發(fā)送到隊列中,此時會在隊列中積壓曹动,然后同步等待緩存更新完成斋日。
這里有一個優(yōu)化點,一個隊列中墓陈,其實多個更新緩存請求串在一起是沒意義的桑驱,因此可以做過濾,如果發(fā)現(xiàn)隊列中已經(jīng)有一個更新緩存的請求了跛蛋,那么就不用再放個更新請求操作進去了熬的,直接等待前面的更新操作請求完成即可。
待那個隊列對應的工作線程完成了上一個操作的數(shù)據(jù)庫的修改之后赊级,才會去執(zhí)行下一個操作押框,也就是緩存更新的操作,此時會從數(shù)據(jù)庫中讀取最新的值理逊,然后寫入緩存中橡伞。
如果請求還在等待時間范圍內(nèi),不斷輪詢發(fā)現(xiàn)可以取到值了晋被,那么就直接返回兑徘;如果請求等待的時間超過一定時長,那么這一次直接從數(shù)據(jù)庫中讀取當前的舊值羡洛。
C.高并發(fā)的場景下挂脑,該解決方案要注意的問題:
m.讀請求長時阻塞
由于讀請求進行了非常輕度的異步化,所以一定要注意讀超時的問題欲侮,每個讀請求必須在超時時間范圍內(nèi)返回崭闲。
該解決方案,最大的風險點在于說威蕉,可能數(shù)據(jù)更新很頻繁刁俭,導致隊列中積壓了大量更新操作在里面,然后讀請求會發(fā)生大量的超時韧涨,最后導致大量的請求直接走數(shù)據(jù)庫牍戚。
m.讀請求并發(fā)量過高
突然間大量讀請求會在幾十毫秒的延時行在服務上,看服務能不能扛的住虑粥,需要多少機器才能扛住最大的極限情況的峰值如孝。
m.多服務實例部署的請求路由
可能這個服務部署了多個實例,那么必須保證說舀奶,執(zhí)行數(shù)據(jù)更新操作暑竟,以及執(zhí)行緩存更新操作的請求斋射,都通過 Nginx 服務器路由到相同的服務實例上育勺。
m.熱點商品的路由問題但荤,導致請求的傾斜
萬一某個商品的讀寫請求特別高,全部打到相同的機器的相同的隊列里面去了涧至,可能會造成某臺機器的壓力過大腹躁。就是說,因為只有在商品數(shù)據(jù)更新的時候才會清空緩存南蓬,然后才會導致讀寫并發(fā)纺非,所以其實要根據(jù)業(yè)務系統(tǒng)去看,如果更新頻率不是太高的話赘方,這個問題的影響并不是特別大烧颖,但是的確可能某些機器的負載會高一些。