數(shù)據(jù)更新策略:到底是先更新數(shù)據(jù)庫還是先更新緩存?
很多小伙伴最近都在問我,在系統(tǒng)中引入緩存后,當(dāng)向數(shù)據(jù)庫中寫入數(shù)據(jù)時,是先寫數(shù)據(jù)庫還是先寫緩存呢?先寫數(shù)據(jù)庫和先寫緩存有什么區(qū)別嗎?今天,我們就一起來聊聊這個話題。
又一個可直接應(yīng)用于生產(chǎn)環(huán)境的熔斷組件項目完結(jié)并上線,點擊鏈接:https://t.zsxq.com/HIE6n 快速學(xué)習(xí),并可直接應(yīng)用于你的生產(chǎn)環(huán)境項目。
從本質(zhì)上講,無論是先寫數(shù)據(jù)庫還是先寫緩存,都是為了保證數(shù)據(jù)庫和緩存的數(shù)據(jù)一致,也就是我們常說的數(shù)據(jù)一致性。
隨著互聯(lián)網(wǎng)的高速發(fā)展,當(dāng)今時代已然從IT時代進(jìn)入到DT時代。互聯(lián)網(wǎng)系統(tǒng)架構(gòu)也已經(jīng)由最初的單體架構(gòu)轉(zhuǎn)變?yōu)榉植际健⑽⒎?wù)架構(gòu)模式。從數(shù)據(jù)體量上來看,各系統(tǒng)存儲的數(shù)據(jù)量越來越大,數(shù)據(jù)的查詢性能越來越低。此時,就需要我們不斷的進(jìn)行優(yōu)化,一種常用的優(yōu)化手段就是引入緩存。而引入緩存后,我們在向數(shù)據(jù)庫插入數(shù)據(jù)時,到底是先更新數(shù)據(jù)庫還是先更新緩存呢?
緩存的一般使用
緩存,從本質(zhì)上講,是為了更好的協(xié)調(diào)兩個速度差異比較大的組件而引入的一種中間緩存層。例如,如果需要將數(shù)據(jù)讀入CPU進(jìn)行計算處理,由于CPU的運算速度是非常快的,而磁盤的IO處理相比于CPU來說,慢了很多數(shù)量級,每次從磁盤讀取數(shù)據(jù),勢必會造成CPU長時間并且頻繁等待磁盤IO。此時,我們就可以通過內(nèi)存來緩和CPU和磁盤之間的速度差異。
圖片
從緩存的使用上來說,一般是按照如下的流程來使用緩存。
圖片
我們也可以表示成如下的序列圖。
圖片
在上面的使用示例中,我們只是簡單的將數(shù)據(jù)放入了緩存,最多為緩存設(shè)置一個過期時間,到期后,緩存自然就會被清除,后續(xù)的請求由于在緩存中獲取不到數(shù)據(jù),又會從數(shù)據(jù)庫中獲取數(shù)據(jù),將數(shù)據(jù)寫入緩存。
但是在后續(xù)更新數(shù)據(jù)的操作中,是更新完數(shù)據(jù)庫,接下來更新緩存還是刪除緩存?又或者是先刪除緩存,再更新數(shù)據(jù)庫?
緩存更新策略
從理論上來說,給緩存設(shè)置過期時間,其實是一種最終一致性的表現(xiàn)。這種方案下,可以對存入緩存的數(shù)據(jù)設(shè)置過期時間,所有的寫操作以數(shù)據(jù)庫為準(zhǔn),對緩存操作只是盡最大努力即可。也就是說如果數(shù)據(jù)庫寫成功,緩存更新失敗,那么只要到達(dá)過期時間,則后面的讀請求自然會從數(shù)據(jù)庫中讀取新值然后回填緩存。這也是一般情況下,使用的最多的一種方式。
先更新數(shù)據(jù)庫再更新緩存
其實,這種方案很多有經(jīng)驗的小伙伴是很反對的,為啥,我們來分析下。
首先,這種方案會有線程安全的問題。
例如,同時有線程A和線程B對數(shù)據(jù)進(jìn)行更新操作,可能會出現(xiàn)下面的執(zhí)行順序。
(1) 線程A更新了數(shù)據(jù)庫
(2) 線程B更新了數(shù)據(jù)庫
(3) 線程B更新了緩存
(4) 線程A更新了緩存
此時就會出現(xiàn)數(shù)據(jù)庫中的數(shù)據(jù)與緩存的數(shù)據(jù)不一致的情況,這是因為線程A先更新了數(shù)據(jù)庫,可能因為網(wǎng)絡(luò)等異常情況,線程B更新完數(shù)據(jù)庫進(jìn)而更新了緩存,當(dāng)線程B更新完緩存后,線程A才更新緩存,這就導(dǎo)致了數(shù)據(jù)庫數(shù)據(jù)與緩存數(shù)據(jù)的不一致。
其次,這種方案也有其不適用的業(yè)務(wù)場景。
首先一個業(yè)務(wù)場景就是數(shù)據(jù)庫寫多讀少的場景,這種場景下采用先更新數(shù)據(jù)庫再更新緩存的策略,就會導(dǎo)致緩存并未被讀取就會被頻繁的更新,極大的浪費了服務(wù)器的性能。
再一個業(yè)務(wù)場景就是數(shù)據(jù)庫中的數(shù)據(jù)不是直接寫入緩存的,而是需要大量的復(fù)雜運算,將運算結(jié)果寫入緩存。如果這種場景下使用先更新數(shù)據(jù)庫再更新緩存的策略,也會造成服務(wù)器資源的浪費。
先刪除緩存再更新數(shù)據(jù)庫
先刪除緩存再更新數(shù)據(jù)庫的方案也存在著線程安全的問題,例如,線程A更新緩存,同時,線程B讀取緩存的數(shù)據(jù)。可能會出現(xiàn)下面的執(zhí)行順序。
(1) 線程A刪除緩存
(2) 線程B查詢緩存,發(fā)現(xiàn)緩存中沒有想要的數(shù)據(jù)
(3) 線程B查詢數(shù)據(jù)庫中的舊數(shù)據(jù)
(4) 線程B將查詢到的舊數(shù)據(jù)寫入緩存
(5) 線程A將新數(shù)據(jù)寫入數(shù)據(jù)庫
此時,就出現(xiàn)了數(shù)據(jù)庫中的數(shù)據(jù)和緩存中的數(shù)據(jù)不一致的情況。如果刪除緩存失敗,也會出現(xiàn)數(shù)據(jù)庫數(shù)據(jù)和緩存數(shù)據(jù)不一致的現(xiàn)象。
先更新數(shù)據(jù)庫再刪除緩存
首先,這種方式也有極小的概率發(fā)生數(shù)據(jù)庫數(shù)據(jù)和緩存數(shù)據(jù)不一致的情況,例如,線程A做查詢操作,線程B執(zhí)行更新操作,其執(zhí)行的順序如下所示。
(1)緩存剛好失效
(2)請求A查詢數(shù)據(jù)庫,獲取到數(shù)據(jù)庫中的舊值
(3)請求B將新值寫入數(shù)據(jù)庫
(4)請求B刪除緩存
(5)請求A將查到的舊值寫入緩存
如果上述順序一旦發(fā)生,就會造成數(shù)據(jù)庫中的數(shù)據(jù)和緩存中的數(shù)據(jù)不一致的情況發(fā)生。
但是,先更新數(shù)據(jù)庫再刪除緩存的策略發(fā)生數(shù)據(jù)庫和緩存數(shù)據(jù)不一致的概率很低,原因就是:(3)的寫數(shù)據(jù)庫操作比步驟(2)的讀數(shù)據(jù)庫操作耗時更短,才有可能使得步驟(4)先于步驟(5)執(zhí)行。但是,往往數(shù)據(jù)庫的讀操作的速度遠(yuǎn)快于寫操作,因此步驟(3)耗時比步驟(2)更短,這一場景很難出現(xiàn)。
如果刪除緩存失敗,也會出現(xiàn)數(shù)據(jù)庫數(shù)據(jù)和緩存數(shù)據(jù)不一致的現(xiàn)象。
這樣說來,貌似三種方案都不安全呀,那我們該如何做呢?最重要的就是需要引入重試機(jī)制。
推薦使用
在實際的生產(chǎn)環(huán)境中,推薦 使用先更新數(shù)據(jù)庫再刪除緩存 的操作。那么,我們該如何解決這種策略下的問題呢?
有兩種方案,一種是在程序邏輯中處理失敗重試的操作;另外,借助于阿里巴巴開源的Canal。
手動失敗重試
圖片
流程如下所示:
(1)更新數(shù)據(jù)庫數(shù)據(jù);
(2)刪除緩存數(shù)據(jù)失敗
(3)將需要刪除的key發(fā)送至消息隊列
(4)自己消費消息,獲得需要刪除的key
(5)繼續(xù)重試刪除操作,直到成功
這種方案有一個缺點,對業(yè)務(wù)線代碼造成大量的侵入。
同步數(shù)據(jù)庫數(shù)據(jù)
先來一張圖,這種圖從整體架構(gòu)上解決了數(shù)據(jù)庫數(shù)據(jù)和緩存數(shù)據(jù)不一致的情況。
圖片
流程如下圖所示:
(1)更新數(shù)據(jù)庫數(shù)據(jù)
(2)數(shù)據(jù)庫將數(shù)據(jù)表數(shù)據(jù)的變更信息寫入binlog日志當(dāng)中
(3)訂閱程序獲取所需要的數(shù)據(jù)以及key
(4)程序邏輯中處理具體的業(yè)務(wù)邏輯,接收訂閱binlog、發(fā)起刪除緩存的請求。
(5)嘗試刪除緩存操作,發(fā)現(xiàn)刪除失敗
(6)將這些信息發(fā)送至消息隊列
(7)重新從消息隊列中獲得該數(shù)據(jù),重試操作