1. 概述
緩存設計是應用系統設計中重要的一環,是通過空間換取時間的一種策略,達到高性能訪問數據的目的;但是緩存的數據並不是時刻存在內存中,當數據發生變化時,如何與數據庫中的數據保持一致,以滿足業務系統要求,本篇將給出具體分析。
2. 強一致與最終一致性
所謂強一致,就是指系統在對外提供服務的過程中,時刻讓緩存數據與數據庫保持一致,這種情況比如秒殺系統,商家后台,他會設置秒殺商品,參與秒殺活動,一旦說他參與了秒殺活動,商品的庫存本來是在數據庫里的,此時必須直接被加載到緩存里,緩存立馬就要可以被使用。最終一致性,就是允許緩存與數據庫在中間一小段時間中有不一致的情況,但是最終兩者是一致的,比如微博的粉絲數,頁面每天的訪問數。本篇重點講最終一致性,強一致的情況后續分析。
3. 緩存與數據庫一致性
3.1 緩存的更新機制
緩存的更新,一般分為被動更新與主動更新,被動更新是指緩存在有效期到后,被淘汰。
被動更新如下步驟:
step1
: 發起方查數據,緩存中沒有,從數據庫中獲取,並寫入緩存,同時設置過期時間t;
step2
: 在t內,所有的查詢,都由緩存提供,所有的寫,直接寫數據庫;
step3
: 當緩存數據到過期時間t后,緩存數據失效。后面的查詢,回到了第1步。
主動更新,一般為調用方發起緩存與數據庫同時更新,緩存分為刪除、更新,數據庫分為更新,通過組合與先后順序,分為如下四種情況:
更新緩存、更新數據庫
,更新數據庫,更新緩存
,刪除緩存,更新數據庫
,更新數據庫,刪除緩存
,下面逐一分析。
3.2 更新緩存、更新數據庫
這種情況,當緩存更新成功,數據庫更新不成功時,數據不一致的風險比較高,所以一般不采用
。
3.2 更新數據庫、更新緩存
當更新完數據庫,緩存的加載前需要通過大量復雜計算才能得出緩存的值,不僅讓發起方阻塞,影響性能;而且如果緩存命中率不高,很少使用,更浪費前期的復雜計算成本與緩存空間,這里就不符合懶加載
的設計思想,故一般也不采用
。
3.3 刪除緩存、更新數據庫
如圖所示,當兩個調用方線程高並發訪問的情況下,A線程先刪除緩存,再更新數據庫,此過程時間較長,B線程在A刪除緩存后,迅速讀取緩存,因緩存每命中,從數據庫中讀取再加載緩存,此時緩存還是舊值,等A線程更新完數據庫后,發現又出現數據不一致的現象。
一般大概率情況下,出現此根源的原因是讀比寫快
,所以這種一般也不采用
,如果非得采用,需要在寫完數據庫之后延遲一段時間再刪除一次緩存,也稱延時雙刪
,延遲多久呢,一般看數據庫的更新時長來決定,此做法也會帶來系統吞吐量下降
。
3.4 更新數據庫,刪除緩存
該方案是比較經典的cache-aside模式,雖然這種方式也會帶來不一致的情況,比如如下場景:
前提:緩存無數據,數據庫有數據。
A:查詢,B:更新
過程如下:
step1
: A查緩存,無數據,去讀數據庫,舊值;
step2
: B更新數據庫為新值;
step3
: B刪除緩存;
step4
: A將舊值寫入緩存。
該場景最終也會出現不一致,產生的根源是是讀比寫慢
,這種是小概率事件,一般很少出現,如果非要解決這種情況,可以采用延遲雙刪,再刪除一次緩存。
3.5 Read/Write Through
上面的方式,數據庫是緩存的來源,主導是數據庫,而 Read/Write Through
模式,相當於緩存占主導。在cache-aside模式中,我們的應用代碼需要維護兩個數據存儲,一個是緩存(Cache),一個是數據庫(Repository)。而Read/Write Through做法是把更新數據庫(Repository)的操作由緩存自己代理了,所以,對於應用層來說,就簡單很多了。可以理解為,應用認為后端就是一個單一的存儲,而存儲自己維護自己的Cache。
Read Through
就是在查詢操作中更新緩存,也就是說,當緩存失效的時候(過期或LRU換出),Cache Aside是由調用方負責把數據加載入緩存,而Read Through則用緩存服務自己來加載,從而對應用方是透明的。
Write Through
, 和Read Through相仿,不過是在更新數據時發生。當有數據更新的時候,如果沒有命中緩存,直接更新數據庫,然后返回。如果命中了緩存,則更新緩存,然后再由Cache自己同步更新數據庫
值得注意的是,該方案在實現過程中,程序啟動時,需將數據庫的數據, 提前放到緩存中,不能等啟動完成,再放緩存中。
3.5 Write Behind
Write Behind 又叫 Write Back。一些了解Linux操作系統內核的同學對write back應該非常熟悉,這不就是Linux文件系統的Page Cache的算法嗎?是的,你看基礎這玩意全都是相通的。所以,底層思想很重要,我已經不是一次說過底層很重要這事了。
Write Behind 思想,一句說就是,在更新數據的時候,只更新緩存,不更新數據庫,而我們的緩存會異步地批量更新數據庫。這個設計的好處就是讓數據的I/O操作速度飛快(因為是直接操作內存),同時帶來吞吐量大幅上升;因為異步,Write Behind 還可以合並對同一個數據的多次操作,所以性能的提高是相當可觀的。
但是,其帶來的問題是,數據不是強一致性的,而且可能會丟失(我們知道Unix/Linux非正常關機會導致數據丟失,就是因為這個事)。在軟件設計上,我們基本上不可能做出一個沒有缺陷的設計,就像算法設計中的時間換空間,空間換時間一個道理,有時候,強一致性和高性能,高可用和高性性是有沖突的。如果說軟件功能模塊的思維是邏輯與實現
,那么軟件架構設計的思維是權衡與取舍
。
4. 總結
(1)上面講的一些模式,具體在實際設計過程中,需要根據場景做權衡,這些東西都是計算機體系結構里的設計,比如CPU的緩存,硬盤文件系統中的緩存,硬盤上的緩存,數據庫中的緩存。基本上來說,這些緩存更新的設計模式都是非常經典的,而且歷經長時間考驗的策略,所以這也就是,工程學上所謂的最佳實踐。
(2)有時候,我們覺得能做宏觀的系統架構的人一定是很有經驗的,其實,宏觀系統架構中的很多設計都來源於這些微觀的東西。比如,雲計算中的很多虛擬化技術的原理,和傳統的虛擬內存不是很像么?Unix下的那些I/O模型,也放大到了架構里的同步異步的模型,還有Unix發明的管道不就是數據流式計算架構嗎?如果你要做好架構,首先你得把計算機體系結構以及很多底層的技術吃透了,應用層的架構一定能從底層找到原型或者影子。
3)在軟件開發或設計中,我非常建議在之前先去參考一下底層軟件已有的設計和思路,比如操作系統、編譯原理、計算機組成原理以及網絡,找到相應的經典設計思路與最佳實踐,吃透了已有的這些東西,再決定是否要重新發明輪子。千萬不要似是而非地,想當然的做軟件設計。
4)上面,我們沒有考慮緩存(Cache)和持久層(Repository)的整體事務的問題。比如,更新Cache成功,更新數據庫失敗了怎么嗎?或是反過來。關於這個事,如果你需要強一致性,你需要使用兩階段提交協議——prepare, commit/rollback,比如Java 7 的XAResource,還有MySQL 5.7的 XA Transaction,有些cache也支持XA,比如EhCache,關於事務問題后續再分析。