先闡明一下Mysql和Redis的關系:Mysql是數據庫,用來持久化數據,一定程度上保證數據的可靠性;Redis是用來當緩存,用來提升數據訪問的性能。
關于如何保證Mysql和Redis中的數據一致(即緩存一致性問題),這是一個非常經典的問題。
使用過緩存的人都應該知道,在實際應用場景中,要想實時刻保證緩存和數據庫中的數據一樣,很難做到。
基本上都是盡可能讓他們的數據在絕大部分時間內保持一致,并保證最終是一致的。
緩存不一致是如何產生的
如果數據一直沒有變更,那么就不會出現緩存不一致的問題。
通常緩存不一致是發生在數據有變更的時候。 因為每次數據變更你需要同時操作數據庫和緩存,而他們又屬于不同的系統,無法做到同時操作成功或失敗,總會有一個時間差。在并發讀寫的時候可能就會出現緩存不一致的問題(理論上通過分布式事務可以保證這一點,不過實際上基本上很少有人這么做)。
雖然沒辦法在數據有變更時,保證緩存和數據庫強一致,但對緩存的更新還是有一定設計方法的,遵循這些設計方法,能夠讓這個不一致的影響時間和影響范圍最小化。
緩存更新的幾種設計
緩存更新的設計方法大概有以下四種:
先刪除緩存,再更新數據庫(這種方法在并發下最容易出現長時間的臟數據,不可取)
先更新數據庫,刪除緩存(Cache Aside Pattern)
只更新緩存,由緩存自己同步更新數據庫(Read/Write Through Pattern)
只更新緩存,由緩存自己異步更新數據庫(Write Behind Cache Pattern)
接下來詳細介紹一些這四種設計方法
先刪除緩存,再更新數據庫
這種方法在并發讀寫的情況下容易出現緩存不一致的問題
?
如上圖所示,其可能的執行流程順序為:
客戶端1 觸發更新數據A的邏輯
客戶端2 觸發查詢數據A的邏輯
客戶端1 刪除緩存中數據A
客戶端2 查詢緩存中數據A,未命中
客戶端2 從數據庫查詢數據A,并更新到緩存中
客戶端1 更新數據庫中數據A
可見,最后緩存中的數據A跟數據庫中的數據A是不一致的,緩存中的數據A是舊的臟數據。
因此一般不建議使用這種方式。
先更新數據庫,再讓緩存失效
這種方法在并發讀寫的情況下,也可能會出現短暫緩存不一致的問題
?
如上圖所示,其可能執行的流程順序為:
客戶端1 觸發更新數據A的邏輯
客戶端2 觸發查詢數據A的邏輯
客戶端3 觸發查詢數據A的邏輯
客戶端1 更新數據庫中數據A
客戶端2 查詢緩存中數據A,命中返回(舊數據)
客戶端1 讓緩存中數據A失效
客戶端3 查詢緩存中數據A,未命中
客戶端3 查詢數據庫中數據A,并更新到緩存中
可見,最后緩存中的數據A和數據庫中的數據A是一致的,理論上可能會出現一小段時間數據不一致,不過這種概率也比較低,大部分的業務也不會有太大的問題。
只更新緩存,由緩存自己同步更新數據庫(Read/Write Through Pattern)
這種方法相當于是業務只更新緩存,再由緩存去同步更新數據庫。 一個Write Through的 例子如下:
?
如上圖所示,其可能執行的流程順序為:
客戶端1 觸發更新數據A的邏輯
客戶端2 觸發查詢數據A的邏輯
客戶端1 更新緩存中數據A,緩存同步更新數據庫中數據A,再返回結果
客戶端2 查詢緩存中數據A,命中返回
Read Through 和 WriteThrough 的流程類似,只是在客戶端查詢數據A時,如果緩存中數據A失效了(過期或被驅逐淘汰),則緩存會同步去數據庫中查詢數據A,并緩存起來,再返回給客戶端
這種方式緩存不一致的概率極低,只不過需要對緩存進行專門的改造。
只更新緩存,由緩存自己異步更新數據庫(Write Behind Cache Pattern)
這種方式性詳單于是業務只操作更新緩存,再由緩存異步去更新數據庫,例如:
?
如上圖所示,其可能的執行流程順序為:
客戶端1 觸發更新數據A的邏輯
客戶端2 觸發查詢數據A的邏輯
客戶端1 更新緩存中的數據A,返回
客戶端2 查詢緩存中的數據A,命中返回
緩存異步更新數據A到數據庫中
這種方式的優勢是讀寫的性能都非常好,基本上只要操作完內存后就返回給客戶端了,但是其是非強一致性,存在丟失數據的情況。
如果在緩存異步將數據更新到數據庫中時,緩存服務掛了,此時未更新到數據庫中的數據就丟失了。
總結
上面講到的幾種緩存更新的設計方式,都是前人總結出來的經驗,這些方式或多或少都有一些弊端,并不完美,實際上也很難有完美的設計。 大家在做系統設計的時候,也不要去追求完美,要有一些取舍,找到一種最適合自己業務場景的方式就行
作者:追光者
|