只要我們使用快取,就必然會面對快取和資料庫間的一致性問題。如果快取中的資料和資料庫的資料不一致,那麼業務應用從快取中讀取的資料就不是最新的資料,對業務的影響可想而知。比如我們把商品的庫存資料存在快取中,如果快取中庫存資料不對,那麼可能就會影響下單操作,這是業務上很難接受的。本篇文章我們來一起聊一聊快取的一致性問題。
如何解決快取不一致
先刪快取再更新資料庫
假設執行緒A刪除快取後,還沒來得及更新資料庫,這時候執行緒B開始讀資料,執行緒B發現快取缺失就只能去讀資料庫,等到執行緒B從資料庫中讀取完資料回塞快取後,執行緒A才開始更新資料庫,此時,快取中的資料是舊值,而資料庫中是最新值,兩者已經不一致了。
這種場景的解決方案是線上程A更新完資料庫的值後,可以讓它sleep一小段時間,再進行一次快取刪除操作,之所以要加上sleep的一段時間,就是為了讓執行緒B能夠先從資料庫讀取出資料然後再把快取miss的資料回塞到快取,然後執行緒A再進行刪除。所以執行緒A的sleep時間就需要大於執行緒B讀取資料再寫入快取的時間。這個時間是多少呢?這個是需要我們在業務中加入打點監控來統計的,根據這個統計值來估算該時間。這樣一來,其他執行緒讀取資料時,會發現快取缺失,就會從資料庫中讀取最新的值。我們把這種模型叫做 "延時雙刪"。
先更新資料庫再刪除快取
如果執行緒A更新了資料庫中的值,但還沒來得及刪除快取中的值,執行緒B這時候開始讀取資料,此時,執行緒B查詢快取時,命中了快取,就會直接使用快取中的值,該值為舊值。不過在這種場景下,如果併發請求量不高的話,其實基本上不會有執行緒讀到舊值,而且執行緒A更新完資料庫後,刪除快取是非常快的操作,所以,這種情況總體對業務影響較小。一般在生產環境中,也推薦大家採用該模式。
重試機制
可以把要刪除的快取值或者要更新的資料庫的值放到訊息佇列中,當應用沒能夠成功地刪除快取或者是更新資料庫的值的時候,可以從訊息佇列中消費這些值,這裡消費訊息佇列的服務叫job,然後再次進行刪除或者更新,起到一個兜底補償的作用,以此來保證最終的一致性。
如果能夠成功地刪除或更新,就需要把這些值從訊息佇列中去除,以免重複操作,此時,我們也可以保證資料庫和快取資料的一致了,否則的話,我們還需要再次進行重試,如果重試超過一定次數還是失敗,這時候一般都需要記錄錯誤日誌或者傳送告警通知。
併發讀寫
首先第一步執行緒A讀取快取,這時候快取沒有命中,由於使用的是cache aside這種模式,所以接下來第二步執行緒A會去讀資料庫,這個時候執行緒B更新資料庫,更新完資料庫後通過set cache更新了快取,最後第五步執行緒A把從資料庫讀到的值通過set cache也更新了快取,但是這時候執行緒A中的資料已經是髒資料了,由於第四步和第五步都是設定快取,導致寫入的值相互覆蓋,並且操作的順序具有不確定性,從而導致了快取不一致情況的發生。
怎麼解決這個問題呢?其實非常地簡單,我們只需要把第五步的set cache操作替換成add cache即可,add cache即setnx操作,只有快取不存在的時候才會成功寫入,相當於加了優先順序,即更新資料庫後的更新快取優先順序更高,而讀資料庫後回塞快取的優先順序較低,從而保證寫操作的最新資料不會被讀操作的回塞資料覆蓋。
結束語
本篇文章說明了在使用快取時最常遇見的一個問題,也就是快取和資料庫不一致的問題,針對這個問題我們列舉了一些可能導致不一致的場景以及對應場景的解決方案,特別地,對於job非同步補償的場景我們可以使用set操作來強行覆蓋快取,保證快取的更新為最新的資料,而對於讀資料庫回塞快取的操作我們一般使用add來更新快取。
希望本篇文章對你有所幫助,謝謝。
每週一、週四更新
程式碼倉庫: https://github.com/zhoushuguang/lebron
專案地址
https://github.com/zeromicro/go-zero
歡迎使用 go-zero
並 star 支援我們!
微信交流群
關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。