一、提前閱讀
討論這個問題之前可以先看下緩存模式(Cache Aside、Read Through、Write Through、Write Behind)這篇文章。
二、先更新緩存,再更新數據庫
1、考慮並發操作:線程A寫,線程B讀
- 1、線程A發起一個寫操作,第一步delete cache
- 2、此時線程B發起一個讀操作,cache miss
- 3、線程B繼續讀數據庫,讀出來一個老數據
- 4、然后老數據入cache
- 5、線程A寫入了最新的數據
這樣以后每次從緩存中讀到的都是老數據,造成數據不一致。
既然這種情況下先刪除緩存會有數據不一致的情況,那我們來試試第一步不刪除緩存而是直接更新緩存試試看。
2、考慮並發操作:線程A寫,線程B寫
- 1、線程A發起一個寫操作,第一步set cache
- 2、線程B發起一個寫操作,第一步set cache
- 3、線程B寫入數據到數據庫
- 4、線程A寫入數據到數據庫
這樣以后每次從緩存中讀到的都是線程B設置的數據,但數據庫中存儲的是線程A寫入的數據,導致數據不一致。
3、小結
可看到先操作緩存不論是先刪除緩存還是先更新緩存都會發生數據不一致的情況,所以不推薦這兩種做法。
從理論上說,只要我們設置了緩存的過期時間,我們就能保證緩存和數據庫的數據最終是一致的。因為只要緩存數據過期了,就會被刪除。隨后讀的時候,因為緩存里沒有,就得去查數據庫的數據,然后將查出來的數據寫入到緩存中。除了設置過期時間,我們還需要做更多的措施來盡量避免數據庫與緩存處於不一致的情況發生。但是設置過期時間是基本操作,只要數據不是靜態數據,就應該給緩存中的此類數據設置過期時間,它並不是解決“先更新緩存,再更新數據庫”造成數據不一致問題的方法。
三、先更新數據庫,再更新緩存
1、考慮並發操作:線程A寫,線程B讀
- 1、線程A發起一個寫操作,第一步寫入數據到數據庫
- 2、線程A第二步delete cache
- 3、線程B發起一個讀操作,cache miss
- 4、線程B從數據庫獲取最新數據
- 5、線程B同時set cache
一個是讀操作,一個是寫操作的並發,首先沒有了文章開始刪除cache數據的操作了,而是先更新了數據庫中的數據,此時緩存依然有效,所以此時讀操作查到的是沒有更新的舊數據,但是更新操作馬上讓緩存失效了,后續的查詢操作再把數據從數據庫中查出來。而不會像文章開頭的那個邏輯產生的問題,即后續的查詢操作一直都在讀舊的數據。
這是標准的Cache Aside 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?》,主要是怕兩個並發的寫操作導致臟數據,這個問題我們下面會說到。
特殊情況:
在高並發的場景下,可能會出現數據庫與緩存數據不一致的的情況,考慮下面情形:
- 1、線程A發起一個寫操作,還未操作數據庫
- 2、緩存剛好失效
- 3、線程B查詢數據庫,得一個舊值
- 4、線程A將新值寫入數據庫
- 5、線程A刪除緩存
- 6、線程B將查到的舊值寫入緩存
但是這個條件需要發生在讀緩存時緩存失效,而且並發着有一個寫操作。而實際上數據庫的寫操作會比讀操作慢得多,而且還要鎖表,而讀操作必須在寫操作前進入數據庫操作,而又要晚於寫操作更新緩存,所有的這些條件都具備的概率其實並不大。
2、考慮並發操作:線程A寫,線程B寫,線程C讀
- 1、線程A發起一個寫操作,第一步寫入數據到數據庫
- 2、線程B發起一個寫操作,第一步寫入數據到數據庫
- 3、線程B第二步delete cache
- 4、線程C發起一個讀操作,cache miss
- 5、線程C從數據庫獲取最新數據
- 6、線程C同時set cache
- 7、線程A第二步delete cache
該情況下由於線程A、B最初都把數據寫入了數據庫,接着都有delete cache,此時如果有線程C來讀數據,你會發現不管線程C的動作做任意順序穿插在A、B動作之間,最后查詢數據最差也就是在線程A、B刪除cache之前獲取到了舊數據,其余都會獲取到新數據,並不會影響后來的請求獲取到新數據。
為什么最后是把緩存的數據刪掉,而不是把更新的數據寫到緩存里。這么做引發的問題是,如果A、B兩個線程同時做數據更新,A先更新了數據庫,B后更新數據庫,則此時數據庫里存的是B的數據。而更新緩存的時候,是B先更新了緩存,而A后更新了緩存,則緩存里是A的數據。這樣緩存和數據庫的數據會發生不一致。
3、小結
- 先操作數據庫再刪除緩存能有讓人可接受的結果,所以最推薦這種做法。
- 先操作緩存再更新數據庫可能造成數據不一致的場景,不推薦這種做法。
四、異常情況
上面的討論與對比都是在更新緩存
和更新數據庫
這兩步操作都成功的情況下敘述的。當然系統正常運行時的操作基本上都是成功的,那么如果兩步操作有其中一步操作失敗了呢?(以先操作數據庫再操作緩存舉例)
- 第一步失敗:這種情況很簡單,不會影響第二步操作,也不會影響數據一致性,直接拋異常出去就好了。
- 第二步失敗:
- 將需要刪除的緩存key發送到消息隊列中
- 另起終端消費隊列消息,獲得需要刪除的緩存key
- 設置重試刪除操作,超過最大重試次數(比如5次)后將消息轉入死信隊列並報警給運維人員