Redis中緩存和數據庫雙寫數據不一致的原因及解決方案
先更新數據庫,還是先更新緩存?
1.先更新數據庫,再更新緩存
2.先更新緩存,再更新數據庫

1.先更新數據庫,再更新緩存
舉個例子,比如【請求A】和【請求B】兩個請求,同時更新【同一條】數據,則可能出現(xiàn)圖中的順序:
【請求A】先將數據庫的數據更新為1,然后在更新緩存前,【請求B】將數據庫的數據更新為2,緊接著把緩存更新為2,然后【請求A】更新緩存為1.此時,數據庫中的數據是2,而緩存中的數據卻是1,出現(xiàn)了緩存和數據庫中的數據不一致的現(xiàn)象

2.先更新緩存,再更新數據庫。
舉個例子,【請求A】和【請求B】兩個請求,同時更新【同一條】數據,則可能出現(xiàn)這樣的順序:
【請求A】先將緩存的數據更新為1,然后在更新數據庫前,【請求B】來了,將緩存的數據更新為2,緊接著把把數據庫更新為2,然后【請求A】將數據庫的數據更新為1.此時,數據庫中的數據是1,而緩存中的數據卻是2,出現(xiàn)了緩存和數據庫中的數據不一致的現(xiàn)象

結論
所以,無論是【先更新數據庫,再更新緩存】,還是【先更新緩存,再更新數據庫】,這兩個方案都存在并發(fā)問題,當兩個請求并發(fā)更新同一條數據的時候,可能會出現(xiàn)緩存和數據庫中的數據不一致的現(xiàn)象
Cache Aside策略
Cache Aside(旁路緩存)策略,該策略可以細分為【讀策略】和【寫策略】
寫策略的步驟:
1.更新數據庫中的數據;
2.刪除緩存中的數據
讀策略的步驟:
1.如果讀取的數據命中了緩存,則直接返回數據
2.如果讀取的數據沒有命中緩存,則從數據庫中讀取數據,然后將數據寫入到緩存,并且返回給用戶
但是【寫策略】中的數據庫和緩存操作又有不同的順序:
1.先刪除緩存,再更新數據庫
2.先更新數據庫,再刪除緩存

1.先刪除緩存,再更新數據庫。
舉個例子,以用戶表的場景來分析。
假設某個用戶的年齡是20,請求A要更新用戶年齡為21,所以它會刪除緩存中的內容。這時,另一個請求B要讀取這個用戶的年齡,它查詢緩存發(fā)現(xiàn)未命中后,會從數據庫中讀取到年齡為20,并且寫入到緩存中,然后請求A繼續(xù)更改數據庫,將用戶的年齡更新為21.
最終,該用戶年齡在緩存中是20(舊值),在數據庫中是21(新值),緩存和數據庫的數據不一致。可以看到,先刪除緩存,再更新數據庫,在【讀+寫】并發(fā)的時候,還是會出現(xiàn)緩存和數據庫的數據不一致的問題

解決方案:
針對【先刪除緩存,再更新數據庫】方法在【讀+寫】并發(fā)請求而造成緩存不一致的解決辦法是【延遲雙刪】:
偽代碼示例。加了個睡眠時間,主要是為了確保請求A在睡眠的時候,請求B能夠在這一段時間內完成【從數據庫讀取數據,再把缺失的緩存寫入緩存】的操作,然后請求A睡眠完,再刪除緩存。所以請求A的睡眠時間就需要大于請求B【從數據庫讀取數據+寫入緩存】的時間。但是具體睡眠多久其實我們是沒法準確預估的,需要進行統(tǒng)計,所以這個方案盡可能保證一致性而已,極端情況下,依然也會出現(xiàn)緩存不一致的現(xiàn)象,因此,還是比較建議用【先更新數據庫,再刪除緩存】的方案
#刪除緩存 redis.delKey(X); #更新數據庫 db.update(X); #睡眠 Thread.sleep(N); #再刪除緩存 redis.delKey(X);
2.先更新數據庫,再刪除緩存
繼續(xù)用【讀+寫】請求的并發(fā)的場景來分析。
假如某個用戶數據在緩存中不存在,請求A讀取讀取數據時從數據庫中查詢到年齡為20,在未寫入緩存中時另一個請求B更新數據。它更新數據庫中的年齡為21,并且清空緩存。這時請求A把數據庫中讀到的年齡為20的數據寫入到緩存中。最終,該用戶年齡在緩存中是20,數據庫中是21,緩存和數據庫數據不一致。

分析
從上面的理論上分析,先更新數據庫,再刪除緩存也是會出現(xiàn)數據不一致性的問題,但是在實際中,這個問題出現(xiàn)的概率并不高。因為緩存的寫入通常要遠遠快于數據庫的寫入,所以在實際中很難出現(xiàn)請求B已經更新了數據庫并且刪除了緩存,請求A才更新完緩存的情況。而一旦請求A早于請求B刪除緩存之前更新了緩存,那么接下來的請求就會因為緩存不命中而從數據庫中重新讀取數據,所以不會出現(xiàn)這種不一致的情況。所以,【先更新數據庫+再刪除緩存】的方案,是可以保證數據一致性的,再加上一個【過期時間】,就算在這期間存在緩存數據不一直,有過期時間來兜底,這樣也能達到最終一致。
【先更新數據庫,再刪除緩存】存在的問題:
前面的分析都是建立再這兩個操作都能同時執(zhí)行成功的情況下,如果在刪除緩存(第二個操作)的時候失敗了,導致緩存中的數據是舊值,如果沒有前面的過期時間兜底的話,后續(xù)的請求就會一直是緩存中的就數據
【先更新數據庫,再刪除緩存】的方案雖然保證了數據庫與緩存的數據一致性,但是每次更新數據的時候,緩存的數據都會被刪除,這樣會對緩存的命中率帶來影響。所以,如果業(yè)務對緩存命中率有很高的要求,可以采用【更新數據庫+更新緩存】的方案,因為更新緩存并不會出現(xiàn)緩存未命中的情況,但是這個方案,前面提到,在兩個更新請求并發(fā)執(zhí)行的時候,會出現(xiàn)數據不一致的問題,因為更新數據庫和更新緩存這兩個操作是獨立的,我們又沒有對操作做任何并發(fā)控制,那么當兩個線程并發(fā)更新它們的話,就會因為寫入順序的不同造成數據不一致需要增加一些手段來解決這個問題,有兩種做法
- 1.在更新緩存前先加個分布式鎖,保證同一時間之運行一個請求更新緩存,就不會產生并發(fā)問題了,但是引入鎖之后,對于寫入性能就會帶來影響
- 2.在更新完緩存時,給緩存加上較短的過期時間,這樣即時出現(xiàn)緩存不一致的情況,緩存的數據也會很快過期,對業(yè)務來說也可以接受
如何保證【先更新數據庫,再刪除緩存】這兩個操作能執(zhí)行成功?
舉個例子:
應用要把數據X的值從1更新為2,先成功更新了數據庫,然后在Redis緩存中刪除X的緩存,但是這個操作卻失敗了,這個時候數據庫中的X的新值為2,Redis中的X的緩存值為1,出現(xiàn)了數據庫和緩存數據不一致的問題。那么后續(xù)有訪問數據X的請求,會先在Redis中查詢,因為緩存中并沒有刪除,所以緩存命中,但是讀到的卻是舊值1.其實不管先操作數據庫,還是先操作緩存,只要第二個操作失敗都會出現(xiàn)數據不一致的問題,解決方案有兩種:
- 1.重試機制
- 2.訂閱MySQL binlog,再操作緩存

1.重試機制。
我們可以引入消息隊列,將第二個操作(刪除緩存)要操作的數據加入到消息隊列,由消費者來操作數據。
- 1.1 如果應用刪除緩存失敗,可以從消息隊列中重新讀取數據,然后再次刪除緩存,這個就是重試機制。當然,如果重試超過一定的次數,還是沒有成功,就需要向業(yè)務層發(fā)送報錯消息了
- 1.2 如果刪除緩存成功,就要把數據從消息隊列中移除,避免重復操作,否則就繼續(xù)重試

2.訂閱MySQL binlog,再操作緩存
【先更新數據庫,再刪除緩存】的策略第一步是更新數據庫,那么更新數據庫成功,就會產生一條變更日志,記錄在binlog里。于是我們就可以通過訂閱binlog日志,拿到具體要操作的數據,然后再執(zhí)行緩存刪除,阿里開源的Cannal中間件就是基于這個實現(xiàn)的。
Cannal模擬MySQL主從復制的交互協(xié)議,把自己偽裝成一個MySQL的從節(jié)點,向MySQL主節(jié)點發(fā)送dump請求,MySQL收到請求后,就會開始推送binlog給Cannal,Cannal解析binlog字節(jié)流之后,轉換為便于讀取的結構化數據,供下游程序訂閱使用.

所以如果要想保證【先更新數據庫,再刪除緩存】策略第二個操作能執(zhí)行成功,我們可以使用【消息隊列來重試緩存的刪除】,或者【訂閱MySQL binlog再操作緩存】,這兩種方法有一個共同的特點,都是采用異步操作緩存
疑問
為什么是刪除緩存,而不是更新緩存?
刪除一個數據,相比更新一個數據更加輕量級,出問題的概率更小。在實際業(yè)務中,緩存的數據可能不是直接來自數據庫表,也許來自多張底層數據表的聚合。比如商品詳情信息,在底層可能會關聯(lián)商品表、價格表、庫存表等,如果更新了一個價格字段,那么就要更新整個數據庫,還要關聯(lián)的去查詢和匯總各個周邊業(yè)務系統(tǒng)的數據,這個操作會非常耗時。從另外一個角度,不是所有的緩存數據都是頻繁訪問的,更新后的緩存可能會長事件不被訪問,所以說,從計算資源和整體性能的考慮,更新的時候刪除緩存,等到下次查詢命中再填充緩存,是一個更好的方案。
系統(tǒng)設計中有一個設計叫Lazy Loading,適用于那些加載代價大的操作,刪除緩存而不是更新緩存,就是懶加載思想的一個應用
以上就是Redis中緩存和數據庫雙寫數據不一致的原因及解決方案的詳細內容,更多關于Redis緩存和數據庫雙寫不一致的資料請關注腳本之家其它相關文章!
相關文章
如何自定義redis工具jar包供其他SpringBoot項目直接使用
這篇文章主要介紹了如何自定義redis工具jar包供其他SpringBoot項目直接使用,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03
Redis源碼解析:集群手動故障轉移、從節(jié)點遷移詳解
這篇文章主要介紹了Redis源碼解析:集群手動故障轉移、從節(jié)點遷移的相關內容,涉及通過集群定時器函數clusterCron實現(xiàn)從節(jié)點遷移等知識,具有一定參考價值,需要的朋友可以了解。2017-10-10

