Previously
緩存系統(tǒng)涉及的問題和知識點是比較多的,我主要分為以下幾個方面來跟大家探討:
- 穩(wěn)定性
- 正確性
- 可觀測性
- 規(guī)范落地和工具建設
上篇 我們分析了緩存系統(tǒng)的穩(wěn)定性,介紹了 go-zero 是怎么解決緩存穿透、緩存擊穿、緩存雪崩問題的。比較淺顯易懂,且具有比較強的實戰(zhàn)意義,推薦一讀。
本文作為系列文章第二篇,主要跟大家探討『緩存數(shù)據(jù)一致性』
緩存正確性
上篇文章提到,我們引入緩存的初衷是為了減小DB壓力,增加系統(tǒng)穩(wěn)定性,所以我們一開始關注的是緩存系統(tǒng)的穩(wěn)定性。當穩(wěn)定性解決之后,一般我們就會面臨數(shù)據(jù)正確性問題,可能會經(jīng)常遇到『明明數(shù)據(jù)更新了,為啥還是顯示老的呢?』這類問題。這就是我們常說的『緩存數(shù)據(jù)一致性』問題了,接下來我們仔細下分析其產(chǎn)生的原因及應對方法。
數(shù)據(jù)更新常見做法
首先,我們講數(shù)據(jù)一致性的前提是我們DB的更新和緩存的刪除不會當成一個原子操作來看待,因為在高并發(fā)的場景下,我們不可能引入一個分布式鎖來把這兩者綁定為一個原子操作,如果綁定的話就會很大程度上影響并發(fā)性能,而且增加系統(tǒng)復雜度,所以我們只會追求數(shù)據(jù)的最終一致性,且本文只針對非追求強一致性要求的高并發(fā)場景,金融支付等同學自行判斷。
常見數(shù)據(jù)更新方式有兩大類,其余基本都是這兩類的變種:
- 先刪緩存,再更新數(shù)據(jù)庫
這種做法是遇到數(shù)據(jù)更新,我們先去刪除緩存,然后再去更新DB,如左圖。讓我們來看一下整個操作的流程:
- A請求需要更新數(shù)據(jù),先刪除對應的緩存,還未更新DB
- B請求來讀取數(shù)據(jù)
- B請求看到緩存里沒有,就去讀取DB并將舊數(shù)據(jù)寫入緩存(臟數(shù)據(jù))
- A請求更新DB
可以看到B請求將臟數(shù)據(jù)寫入了緩存,如果這是一個讀多寫少的數(shù)據(jù),可能臟數(shù)據(jù)會存在比較長的時間(要么有后續(xù)更新,要么等待緩存過期),這是業(yè)務上不能接受的。
- 先更新數(shù)據(jù)庫,再刪除緩存
上圖的右側(cè)部分可以看到在A更新DB和刪除緩存之間B請求會讀取到老數(shù)據(jù),因為此時A操作還沒有完成,并且這種讀到老數(shù)據(jù)的時間是非常短的,可以滿足數(shù)據(jù)最終一致性要求。
上圖可以看到我們用的是刪除緩存,而不是更新緩存,原因如下圖:
上圖我用操作代替了刪除或更新,當我們做刪除操作時,A先刪還是B先刪沒有關系,因為后續(xù)讀取請求都會從DB加載出最新數(shù)據(jù);但是當我們對緩存做的是更新操作時,就會對A先更新緩存還是B先更新緩存敏感了,如果A后更新,那么緩存里就又存在臟數(shù)據(jù)了,所以 go-zero 只使用刪除緩存的方式。
我們來一起看看完整的請求處理流程:
注意:不同顏色代表不同請求。
- 請求1更新DB
- 請求2查詢同一個數(shù)據(jù),返回了老的數(shù)據(jù),這個短時間內(nèi)返回舊數(shù)據(jù)是可以接受的,滿足最終一致性
- 請求1刪除緩存
- 請求3再來請求時緩存里沒有,就會查詢數(shù)據(jù)庫,并回寫緩存再返回結(jié)果
- 后續(xù)的請求就會直接讀取緩存了
另外留一個問題大家可以思考下,對于下圖的場景,我們該怎么應對?
如果你有好的解決方法或者想知道怎么解決,歡迎 go-zero 社區(qū)微信群內(nèi)交流,授人以魚不如授人以漁,求解的過程必將讓你收獲更多~~
未完待續(xù)
本文跟大家一起討論了緩存數(shù)據(jù)一致性問題,下一篇我來跟大家一起討論緩存系統(tǒng)的監(jiān)控以及如何讓緩存代碼更規(guī)范、更少bug。
所有這些問題的解決方法都已包含在 go-zero 微服務框架里,如果你想要更好的了解 go-zero 項目,歡迎前往官方網(wǎng)站上學習具體的示例。
視頻回放地址
ArchSummit架構師峰會-海量并發(fā)下的緩存架構設計
項目地址
https://github.com/tal-tech/go-zero
歡迎使用 go-zero 并 star 支持我們!
微信交流群
關注『微服務實踐』公眾號并點擊 進群 獲取社區(qū)群二維碼。
go-zero 系列文章見『微服務實踐』公眾號