正常我們大家使用緩存都是這個原理,即:
- 如果我們的數據在緩存里邊有,那么就直接取緩存的。
- 如果緩存里沒有我們想要的數據,我們會先去查詢數據庫,然后 將數據庫查出來的數據寫到緩存中。
- 最后將數據返回給請求。
如果僅僅查詢的話,緩存的數據和數據庫的數據是沒問題的。但是,當我們要 更新 時候呢?各種情況很可能就 造成數據庫 和 緩存的數據不一致了。
從理論上說,只要我們設置了 鍵的過期時間,我們就能保證緩存 和 數據庫的數據 最終是一致 的。因為只要緩存數據過期了,就會被刪除。隨后讀的時候,因為緩存里沒有,就可以查數據庫的數據,然后將數據庫查出來的數據寫入到緩存中。
除了設置過期時間,我們還需要做更多的措施來 盡量避免 數據庫 與 緩存處於不一致的情況發生。
1、更新操作
一般來說,執行更新操作時,我們會有兩種選擇:
- 先操作數據庫,再操作緩存
- 先操作緩存,再操作數據庫
首先,要明確的是,無論我們選擇哪個,我們都希望這 兩個操作要么同時成功,要么同時失敗。所以,這會演變成一個 分布式事務 的問題。
所有,如果原子性被破壞,可能會有以下的情況:
-
- 操作數據庫成功了,操作緩存失敗
- 操作緩存成功了,操作數據庫失敗
1.1、操作緩存
操作緩存也有兩種方案:
1、更新緩存
2、刪除緩存
一般我們都是采取 刪除緩存 策略的,原因如下:
1、高並發環境下,無論是先操作數據庫還是后操作數據庫而言,如果加上更新緩存,那就 更加容易 導致數據庫與緩存數據不一致問題。(刪除緩存直接且簡單很多)
2、如果每次更新了數據庫,都要更新緩存【這里指的是頻繁更新的場景,這會耗費一定的性能】,倒不如直接刪除掉。等再次讀取時,緩存里沒有,那我到數據庫找,在數據庫找到再寫到緩存里邊(體現懶加載)
基於這兩點,對於緩存在更新時而言,都是建議執行刪除操作!
1.2、先更新數據庫,再刪除緩存
正常的情況是這樣的:
1. 先操作數據庫,成功;
2. 再刪除緩存,也成功;
如果原子性被破壞了:
1. 第一步成功(操作數據庫),第二步失敗(刪除緩存),會導致 數據庫里是新數據,而緩存里是舊數據。
2. 如果第一步(操作數據庫)就失敗了,我們可以直接返回錯誤(Exception),不會出現數據不一致。
如果在高並發的場景下,出現數據庫與緩存數據不一致的 概率特別低,也不是沒有:
1. 緩存 剛好 失效
2. 線程A查詢數據庫,得一個舊值
3. 線程B將新值寫入數據庫
4. 線程B刪除緩存
5. 線程A將查到的舊值寫入緩存
要達成上述情況,還是說一句 概率特別低:
因為這個條件需要發生在讀緩存時緩存失效,而且並發着有一個寫操作。而實際上數據庫的寫操作會比讀操作慢得多,而且還要鎖表,而讀操作必需在寫操作前進入數據庫操作,而又要晚於寫操作更新緩存,所有的這些條件都具備的概率基本並不大。
刪除緩存失敗的解決思路:
1. 將需要刪除的key發送到消息隊列中
2. 自己消費消息,獲得需要刪除的key
3. 不斷重試刪除操作,直到成功
1.3、先刪除緩存,再更新數據庫
正常情況是這樣的:
1. 先刪除緩存,成功;
2. 再更新數據庫,也成功;
如果原子性被破壞了:
1. 第一步成功(刪除緩存),第二步失敗(更新數據庫),數據庫和緩存的數據還是一致的。
2. 如果第一步(刪除緩存)就失敗了,我們可以直接返回錯誤(Exception),數據庫和緩存的數據還是一致的。
看起來是很美好,但是我們在並發場景下分析一下,就知道還是有問題的了:
1. 線程A刪除了緩存
2. 線程B查詢,發現緩存已不存在
3. 線程B去數據庫查詢得到舊值
4. 線程B將舊值寫入緩存
5. 線程A將新值寫入數據庫
所以也會導致數據庫和緩存不一致的問題。
並發下解決數據庫與緩存不一致的思路:
將刪除緩存、修改數據庫、讀取緩存等的操作積壓到 隊列 里邊,實現 串行化。
2、總結解決方案
方案一:
流程如下所示:
(1)更新數據庫數據;
(2)緩存因為種種問題刪除失敗
(3)將需要刪除的key發送至消息隊列
(4)自己消費消息,獲得需要刪除的key
(5)繼續重試刪除操作,直到成功
然而,該方案有一個缺點,對業務線代碼造成大量的侵入。於是有了方案二,在方案二中,啟動一個訂閱程序去訂閱數據庫的binlog,獲得需要操作的數據。在應用程序中,另起一段程序,獲得這個訂閱程序傳來的信息,進行刪除緩存操作。
方案二:
流程如下圖所示:
(1)更新數據庫數據
(2)數據庫會將操作信息寫入binlog日志當中
(3)訂閱程序提取出所需要的數據以及key
(4)另起一段非業務代碼,獲得該信息
(5)嘗試刪除緩存操作,發現刪除失敗
(6)將這些信息發送至消息隊列
(7)重新從消息隊列中獲得該數據,重試操作。
備注說明:
上述的訂閱binlog程序在mysql中有現成的中間件叫canal,可以完成訂閱binlog日志的功能。另外,重試機制,采用的是消息隊列的方式。如果對一致性要求不是很高,直接在程序中另起一個線程,每隔一段時間去重試即可,這些大家可以靈活自由發揮,只是提供一個思路。
3、總結
本文其實是對目前互聯網中已有的一致性方案,進行了一個總結。
對於先刪緩存,再更新數據庫的更新策略,還有方案提出維護一個內存隊列的方式,看了一下,覺得實現異常復雜,沒有必要,因此沒有必要在文中給出。
4、面試體驗
面試官:你在實際項目中使用緩存有遇到什么問題或者會遇到什么問題你知道嗎?
我:緩存和數據庫數據一致性問題:分布式環境下非常容易出現緩存和數據庫間數據一致性問題,針對這一點,如果項目對緩存的要求是強一致性的,那么就不要使用緩存。我們只能采取合適的策略來降低緩存和數據庫間數據不一致的概率,而無法保證兩者間的強一致性。合適的策略包括合適的緩存更新策略,更新數據庫后及時更新緩存、緩存失敗時增加重試機制。具體方案策略可以參考上面第二種方案。