go-zero微服務(wù)實(shí)戰(zhàn)系列(六、緩存一致性保證)
只要我們使用緩存,就必然會面對緩存和數(shù)據(jù)庫間的一致性問題。如果緩存中的數(shù)據(jù)和數(shù)據(jù)庫的數(shù)據(jù)不一致,那么業(yè)務(wù)應(yīng)用從緩存中讀取的數(shù)據(jù)就不是最新的數(shù)據(jù),對業(yè)務(wù)的影響可想而知。比如我們把商品的庫存數(shù)據(jù)存在緩存中,如果緩存中庫存數(shù)據(jù)不對,那么可能就會影響下單操作,這是業(yè)務(wù)上很難接受的。本篇文章我們來一起聊一聊緩存的一致性問題。
如何解決緩存不一致
先刪緩存再更新數(shù)據(jù)庫
假設(shè)線程A刪除緩存后,還沒來得及更新數(shù)據(jù)庫,這時候線程B開始讀數(shù)據(jù),線程B發(fā)現(xiàn)緩存缺失就只能去讀數(shù)據(jù)庫,等到線程B從數(shù)據(jù)庫中讀取完數(shù)據(jù)回塞緩存后,線程A才開始更新數(shù)據(jù)庫,此時,緩存中的數(shù)據(jù)是舊值,而數(shù)據(jù)庫中是最新值,兩者已經(jīng)不一致了。

這種場景的解決方案是在線程A更新完數(shù)據(jù)庫的值后,可以讓它sleep一小段時間,再進(jìn)行一次緩存刪除操作,之所以要加上sleep的一段時間,就是為了讓線程B能夠先從數(shù)據(jù)庫讀取出數(shù)據(jù)然后再把緩存miss的數(shù)據(jù)回塞到緩存,然后線程A再進(jìn)行刪除。所以線程A的sleep時間就需要大于線程B讀取數(shù)據(jù)再寫入緩存的時間。這個時間是多少呢?這個是需要我們在業(yè)務(wù)中加入打點(diǎn)監(jiān)控來統(tǒng)計的,根據(jù)這個統(tǒng)計值來估算該時間。這樣一來,其他線程讀取數(shù)據(jù)時,會發(fā)現(xiàn)緩存缺失,就會從數(shù)據(jù)庫中讀取最新的值。我們把這種模型叫做 "延時雙刪"。
先更新數(shù)據(jù)庫再刪除緩存
如果線程A更新了數(shù)據(jù)庫中的值,但還沒來得及刪除緩存中的值,線程B這時候開始讀取數(shù)據(jù),此時,線程B查詢緩存時,命中了緩存,就會直接使用緩存中的值,該值為舊值。不過在這種場景下,如果并發(fā)請求量不高的話,其實(shí)基本上不會有線程讀到舊值,而且線程A更新完數(shù)據(jù)庫后,刪除緩存是非常快的操作,所以,這種情況總體對業(yè)務(wù)影響較小。一般在生產(chǎn)環(huán)境中,也推薦大家采用該模式。

重試機(jī)制
可以把要刪除的緩存值或者要更新的數(shù)據(jù)庫的值放到消息隊列中,當(dāng)應(yīng)用沒能夠成功地刪除緩存或者是更新數(shù)據(jù)庫的值的時候,可以從消息隊列中消費(fèi)這些值,這里消費(fèi)消息隊列的服務(wù)叫job,然后再次進(jìn)行刪除或者更新,起到一個兜底補(bǔ)償?shù)淖饔?,以此來保證最終的一致性。
如果能夠成功地刪除或更新,就需要把這些值從消息隊列中去除,以免重復(fù)操作,此時,我們也可以保證數(shù)據(jù)庫和緩存數(shù)據(jù)的一致了,否則的話,我們還需要再次進(jìn)行重試,如果重試超過一定次數(shù)還是失敗,這時候一般都需要記錄錯誤日志或者發(fā)送告警通知。

并發(fā)讀寫
首先第一步線程A讀取緩存,這時候緩存沒有命中,由于使用的是cache aside這種模式,所以接下來第二步線程A會去讀數(shù)據(jù)庫,這個時候線程B更新數(shù)據(jù)庫,更新完數(shù)據(jù)庫后通過set cache更新了緩存,最后第五步線程A把從數(shù)據(jù)庫讀到的值通過set cache也更新了緩存,但是這時候線程A中的數(shù)據(jù)已經(jīng)是臟數(shù)據(jù)了,由于第四步和第五步都是設(shè)置緩存,導(dǎo)致寫入的值相互覆蓋,并且操作的順序具有不確定性,從而導(dǎo)致了緩存不一致情況的發(fā)生。

怎么解決這個問題呢?其實(shí)非常地簡單,我們只需要把第五步的set cache操作替換成add cache即可,add cache即setnx操作,只有緩存不存在的時候才會成功寫入,相當(dāng)于加了優(yōu)先級,即更新數(shù)據(jù)庫后的更新緩存優(yōu)先級更高,而讀數(shù)據(jù)庫后回塞緩存的優(yōu)先級較低,從而保證寫操作的最新數(shù)據(jù)不會被讀操作的回塞數(shù)據(jù)覆蓋。

結(jié)束語
本篇文章說明了在使用緩存時最常遇見的一個問題,也就是緩存和數(shù)據(jù)庫不一致的問題,針對這個問題我們列舉了一些可能導(dǎo)致不一致的場景以及對應(yīng)場景的解決方案,特別地,對于job異步補(bǔ)償?shù)膱鼍拔覀兛梢允褂胹et操作來強(qiáng)行覆蓋緩存,保證緩存的更新為最新的數(shù)據(jù),而對于讀數(shù)據(jù)庫回塞緩存的操作我們一般使用add來更新緩存。
希望本篇文章對你有所幫助,謝謝。
代碼倉庫: https://github.com/zhoushuguang/lebron
推薦閱讀
