保證緩存與數據庫的數據一致性不是很容易

靈魂拷問
  • 保證緩存和數據庫的一致性很簡單嗎?
  • 有哪些方式能保證緩存和數據庫的一致性呢?
  • 若是發生了緩存和數據庫數據不一致的狀況怎麼辦呢?

在上篇文章咱們介紹了緩存的定義分類以及優缺點等,若是還沒看的同窗能夠移步這裏node

據說你會緩存?mysql

當咱們的系統引入緩存組件以後,性能獲得了大幅度提高,可是隨之而來的是代碼須要引入必定的複雜度,好比緩存的更新策略,寫入策略,過時策略等,而其中最可能致使程序員加班的莫過於緩存和數據庫的一致性問題了,既:緩存中的數據和數據庫中的數據不一致。程序員

一致性問題

說到一致性問題,這算是分佈式系統中不可避免的一個痛點,或者說分佈式系統自然就自帶了數據一致性問題,雖然能夠利用不少分佈式事務解決方案來作到一致性,可是實際的系統架構設計中,我仍是推崇避免分佈式事務。緩存和數據庫數據的一致性在產生原理上和分佈式相似,其實能夠把他們兩個的關係看作是分佈式系統中的兩個操做節點。redis

凡是處於不一樣物理位置的兩個操做,若是操做的是相同數據,都會遇到一致性問題

產生數據一致性問題的根本緣由是對一個數據的多個操做過程,緩存和數據庫數據的一致性也是這個原理,系統中最多見的操做流程是這樣的:算法

  • 數據的請求首先查詢緩存中是否存在該數據
  • 若是數據命中緩存(在緩存中存在)則直接返回數據,若是數據沒有命中緩存(緩存中不存在),則去數據庫中取數據
  • 從數據庫中取回數據,而後把數據寫入緩存

好圖

從圖中能夠清楚的看到,對數據庫的操做和對緩存的操做是兩個不一樣階段的操做,在任何一個操做過程當中都會發生線程安全問題。好比說:sql

  • 當兩個線程同時查詢緩存的時候,可能會發生兩個線程都沒有命中緩存的問題
  • 若是兩個線程都沒有命中緩存就會發生同時查詢數據庫的問題
  • 接着就會發生兩個線程同時回寫緩存的問題

而這還不是最致命的,畢竟兩個線程同時查詢數據庫,同時回寫緩存數據在多數狀況下緩存數據和數據庫數據還能保持一致。最要命的是若是是兩個線程都進行更新操做,最多見的更新過程是先更新數據庫,而後更新緩存。下面就以最多見的用戶積分場景爲例,每一個用戶都有本身的積分,假如發生如下過程:數據庫

  • 線程A根據業務會把用戶id爲1的積分更新成100
  • 線程B根據業務會把用戶id爲1的積分更新成200
  • 在數據庫層面,線程A和線程B確定不存在併發狀況,由於數據庫用鎖來保證了ACID(假如是mysql等關係型數據庫),不管數據庫中最終的值是100仍是200,咱們都假設正確。
  • 假設線程B在A以後更新數據庫,則數據庫中的值爲200
  • 線程A和線程B在回寫緩存過程當中,極可能會發生線程A在線程B以後操做緩存的狀況(由於網絡調用存在不肯定性),這個時候緩存內的值會被更新成100,發生了緩存和數據庫不一致的狀況
經過以上案例可見,解決緩存和數據庫數據不一致的根本解決方案是須要把兩個操做合併成邏輯上能保證事務的一個操做

兩個操做看作一個操做

分佈式鎖

在平時開發中,利用分佈式鎖可能算是比較常見的解決方案了。利用分佈式鎖把緩存操做和數據庫操做封裝爲邏輯上的一個操做能夠保證數據的一致性,具體流程爲:設計模式

  • 每一個想要操做緩存和數據庫的線程都必須先申請分佈式鎖
  • 若是成功得到鎖,則進行數據庫和緩存操做,操做完畢釋放鎖
  • 若是沒有得到鎖,根據不一樣業務能夠選擇阻塞等待或者輪訓,或者直接返回的策略

image

利用分佈式鎖是解決分佈式事務的一種方案,可是在必定程度上會下降系統的性能,並且分佈式鎖的設計要考慮到down機和死鎖的意外狀況,而最多見的分佈式鎖就是利用redis,可是也會有很多坑,具體能夠參考以前的文章緩存

redis作分佈式鎖可能不那麼簡單安全

刪除緩存

相對於分佈式鎖的方案,而程序員實際中最喜歡使用的仍是刪除緩存的方式,在一個可能會發生不一致的場景下,咱們會以數據庫爲主,在操做完數據庫以後,不去更新緩存,而是刪除緩存。這在必定意義上至關於只操做數據庫,把須要維護的兩個數據源變成了一個數據源。

image

這種方式要求必須先操做數據庫,後操做緩存,否則的話發生不一致的概率會大不少。爲何這麼說呢?由於就算是先操做數據庫也會有發生不一致的概率,可是畢竟在整個操做過程當中,刪除緩存的操做只佔整個流程時間的一小部分而已,並且咱們能夠利用緩存的過時時間來保證數據的最終一致性,因此在一些能夠容忍數據短暫不一致的場景下能夠採用這種方案的。

刪除緩存方案帶來的另一個劣勢是:若是一樣的數據會被頻繁更新,緩存會被頻繁刪除,當有讀請求的時候又會被頻繁的從數據庫加載,因此這種方案適用於那種對緩存命中率不敏感的系統中。

單線程

發生緩存和數據庫不一致的緣由在於多個線程的同時操做,若是相同的數據始終只會有一個線程去操做,不一致的狀況就會避免了,好比nodejs,能夠充分利用nodejs單線程的優點。提到單線程不能不提一下Actor模型,actor模型在對於一樣的對象上能夠看作是單線程模式,具體有興趣的同窗能夠查看以前的推文

分佈式高併發下Actor模型如此優秀

單線程的模式基本上和分佈式鎖的方案相似,只不過單線程不須要鎖就能夠實現操做的順序化,這也是單線程的優點所在。

其餘方案

若是是以緩存爲主呢?假如咱們的應用程序只和緩存組件通訊,至於持久化數據庫由專門的程序負責,這樣行不行呢?在理論上是能夠的

image

不過這種方案須要考慮幾個方面:

  • 數據從緩存持久化到數據採用什麼樣的解決方案,是同步進行仍是異步進行呢?
  • 在新數據請求的時候,若是緩存不存在,要採用什麼樣的方式來填充數據
  • 若是緩存模塊掛掉了該怎麼辦?

以緩存爲主的方案的優點是數據優先進入IO速度快的設備,對於那些請求量大,可是能夠容忍必定數據丟失的應用很是合適,好比應用log數據的收集系統,這種系統其中一個最大的特色就是能夠容忍必定數據的丟失,可是併發的請求數會很是大。因此咱們就能夠利用緩存設備前置的方案來應對這種應用場景

更多精彩文章

image

相關文章
相關標籤/搜索