如何保證緩存和數據庫的一致性

看到好些人在寫更新緩存數據代碼時,先刪除緩存,而後再更新數據庫,然後續的操做會把數據再裝載的緩存中。然而,這個是邏輯是錯誤的。試想,兩個併發操做,一個是更新操做,另外一個是查詢操做,更新操做刪除緩存後,查詢操做沒有命中緩存,先把老數據讀出來後放到緩存中,而後更新操做更新了數據庫。因而,在緩存中的數據仍是老的數據,致使緩存中的數據是髒的,並且還一直這樣髒下去了。mysql

這篇文章說一下幾個緩存更新的Design Pattern(讓咱們多一些套路吧)。redis

這裏,咱們先不討論更新緩存和更新數據這兩個事是一個事務的事,或是會有失敗的可能,咱們先假設更新數據庫和更新緩存均可以成功的狀況(咱們先把成功的代碼邏輯先寫對)。算法

更新緩存的的Design Pattern有四種:sql

  • Cache aside (旁路緩存 )數據庫

  • Read through後端

  • Write through設計模式

  • Write behind caching緩存

Cache Aside Pattern

這是最經常使用最經常使用的pattern了。其具體邏輯以下:微信

  • 失效:應用程序先從cache取數據,沒有獲得,則從數據庫中取數據,成功後,放到緩存中。架構

  • 命中:應用程序從cache中取數據,取到後返回。

  • 更新:先把數據存到數據庫中,成功後,再讓緩存失效

注意,咱們的更新是先更新數據庫,成功後,讓緩存失效。那麼,這種方式是否能夠沒有文章前面提到過的那個問題呢?咱們能夠腦補一下。

一個是查詢操做,一個是更新操做的併發,首先,沒有了刪除cache數據的操做了,而是先更新了數據庫中的數據,此時,緩存依然有效,因此,併發的查詢操做拿的是沒有更新的數據,可是,更新操做立刻讓緩存的失效了,後續的查詢操做再把數據從數據庫中拉出來。而不會像文章開頭的那個邏輯產生的問題,後續的查詢操做一直都在取老的數據。

這是標準的design pattern,包括Facebook的論文《Scaling Memcache at Facebook》也使用了這個策略。爲何不是寫完數據庫後更新緩存?你能夠看一下Quora上的這個問答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》,主要是怕兩個併發的寫操做致使髒數據。

那麼,是否是Cache Aside這個就不會有併發問題了?不是的,好比,一個是讀操做,可是沒有命中緩存,而後就到數據庫中取數據,此時來了一個寫操做,寫完數據庫後,讓緩存失效,而後,以前的那個讀操做再把老的數據放進去,因此,會形成髒數據。

但,這個case理論上會出現,不過,實際上出現的機率可能很是低,由於這個條件須要發生在讀緩存時緩存失效,並且併發着有一個寫操做。而實際上數據庫的寫操做會比讀操做慢得多,並且還要鎖表,而讀操做必需在寫操做前進入數據庫操做,而又要晚於寫操做更新緩存,全部的這些條件都具有的機率基本並不大。

因此,這也就是Quora上的那個答案裏說的,要麼經過2PC或是Paxos協議保證一致性,要麼就是拼命的下降併發時髒數據的機率,而Facebook使用了這個下降機率的玩法,由於2PC太慢,而Paxos太複雜。固然,最好仍是爲緩存設置上過時時間。

Read/Write Through Pattern

咱們能夠看到,在上面的Cache Aside套路中,咱們的應用代碼須要維護兩個數據存儲,一個是緩存(Cache),一個是數據庫(Repository)。因此,應用程序比較囉嗦。而Read/Write Through套路是把更新數據庫(Repository)的操做由緩存本身代理了,因此,對於應用層來講,就簡單不少了。能夠理解爲,應用認爲後端就是一個單一的存儲,而存儲本身維護本身的Cache。

Read Through

Read Through 套路就是在查詢操做中更新緩存,也就是說,當緩存失效的時候(過時或LRU換出),Cache Aside是由調用方負責把數據加載入緩存,而Read Through則用緩存服務本身來加載,從而對應用方是透明的。

Write Through

Write Through 套路和Read Through相仿,不過是在更新數據時發生。當有數據更新的時候,若是沒有命中緩存,直接更新數據庫,而後返回。若是命中了緩存,則更新緩存,而後再由Cache本身更新數據庫(這是一個同步操做)
下圖自來Wikipedia的Cache詞條。其中的Memory你能夠理解爲就是咱們例子裏的數據庫。

 

 

Write Behind Caching Pattern

Write Behind 又叫 Write Back。一些瞭解Linux操做系統內核的同窗對write back應該很是熟悉,這不就是Linux文件系統的Page Cache的算法嗎?是的,你看基礎這玩意全都是相通的。因此,基礎很重要,我已經不是一次說過基礎很重要這事了。

Write Back套路,一句說就是,在更新數據的時候,只更新緩存,不更新數據庫,而咱們的緩存會異步地批量更新數據庫。這個設計的好處就是讓數據的I/O操做飛快無比(由於直接操做內存嘛 ),由於異步,write backg還能夠合併對同一個數據的屢次操做,因此性能的提升是至關可觀的。

可是,其帶來的問題是,數據不是強一致性的,並且可能會丟失(咱們知道Unix/Linux非正常關機會致使數據丟失,就是由於這個事)。在軟件設計上,咱們基本上不可能作出一個沒有缺陷的設計,就像算法設計中的時間換空間,空間換時間一個道理,有時候,強一致性和高性能,高可用和高性性是有衝突的。軟件設計歷來都是取捨Trade-Off。

另外,Write Back實現邏輯比較複雜,由於他須要track有哪數據是被更新了的,須要刷到持久層上。操做系統的write back會在僅當這個cache須要失效的時候,纔會被真正持久起來,好比,內存不夠了,或是進程退出了等狀況,這又叫lazy write。

在wikipedia上有一張write back的流程圖,基本邏輯以下:


 

 

再多嘮叨一些

1)上面講的這些Design Pattern,其實並非軟件架構裏的mysql數據庫和memcache/redis的更新策略,這些東西都是計算機體系結構裏的設計,好比CPU的緩存,硬盤文件系統中的緩存,硬盤上的緩存,數據庫中的緩存。基本上來講,這些緩存更新的設計模式都是很是老古董的,並且歷經長時間考驗的策略,因此這也就是,工程學上所謂的Best Practice,聽從就行了。

2)有時候,咱們以爲能作宏觀的系統架構的人必定是頗有經驗的,其實,宏觀系統架構中的不少設計都來源於這些微觀的東西。好比,雲計算中的不少虛擬化技術的原理,和傳統的虛擬內存不是很像麼?Unix下的那些I/O模型,也放大到了架構裏的同步異步的模型,還有Unix發明的管道不就是數據流式計算架構嗎?TCP的好些設計也用在不一樣系統間的通信中,仔細看看這些微觀層面,你會發現有不少設計都很是精妙……因此,請容許我在這裏放句觀點鮮明的話——若是你要作好架構,首先你得把計算機體系結構以及不少老古董的基礎技術吃透了。

3)在軟件開發或設計中,我很是建議在以前先去參考一下已有的設計和思路,看看相應的guideline,best practice或design pattern,吃透了已有的這些東西,再決定是否要從新發明輪子。千萬不要似是而非地,想固然的作軟件設計。

4)上面,咱們沒有考慮緩存(Cache)和持久層(Repository)的總體事務的問題。好比,更新Cache成功,更新數據庫失敗了怎麼嗎?或是反過來。關於這個事,若是你須要強一致性,你須要使用「兩階段提交協議」——prepare, commit/rollback,好比Java 7 的XAResource,還有MySQL 5.7的 XA Transaction,有些cache也支持XA,好比EhCache。固然,XA這樣的強一致性的玩法會致使性能降低。


本文分享自微信公衆號 - 互聯網後端架構(fullstack888)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索