分散式快取--快取與資料庫一致性方案

小豬爸爸發表於2022-04-11

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 Back套路,一句說就是,在更新資料的時候,只更新快取,不更新資料庫,而我們的快取會非同步地批量更新資料庫。這個設計的好處就是讓資料的I/O操作飛快無比(因為直接操作記憶體嘛 ),因為非同步,write backg還可以合併對同一個資料的多次操作,所以效能的提高是相當可觀的。

但是,其帶來的問題是,資料不是強一致性的,而且可能會丟失(我們知道Unix/Linux非正常關機會導致資料丟失,就是因為這個事)。在軟體設計上,我們基本上不可能做出一個沒有缺陷的設計,就像演算法設計中的時間換空間,空間換時間一個道理,有時候,強一致性和高效能,高可用和高性性是有衝突的。軟體設計從來都是取捨Trade-Off。

4. 總結

(1)上面講的一些模式,具體在實際設計過程中,需要根據場景做權衡,這些東西都是計算機體系結構裡的設計,比如CPU的快取,硬碟檔案系統中的快取,硬碟上的快取,資料庫中的快取。基本上來說,這些快取更新的設計模式都是非常經典的,而且歷經長時間考驗的策略,所以這也就是,工程學上所謂的最佳實踐。
(2)有時候,我們覺得能做巨集觀的系統架構的人一定是很有經驗的,其實,巨集觀系統架構中的很多設計都來源於這些微觀的東西。比如,雲端計算中的很多虛擬化技術的原理,和傳統的虛擬記憶體不是很像麼?Unix下的那些I/O模型,也放大到了架構裡的同步非同步的模型,還有Unix發明的管道不就是資料流式計算架構嗎?TCP的好些設計也用在不同系統間的通訊中,仔細看看這些微觀層面,你會發現有很多設計都非常精妙……所以,請允許我在這裡放句觀點鮮明的話——如果你要做好架構,首先你得把計算機體系結構以及很多底層的技術吃透了,應用層的架構一定能從底層找到原型或者影子。

3)在軟體開發或設計中,我非常建議在之前先去參考一下已有的設計和思路,看看相應的guideline,best practice或design pattern,吃透了已有的這些東西,再決定是否要重新發明輪子。千萬不要似是而非地,想當然的做軟體設計。

4)上面,我們沒有考慮快取(Cache)和持久層(Repository)的整體事務的問題。比如,更新Cache成功,更新資料庫失敗了怎麼嗎?或是反過來。關於這個事,如果你需要強一致性,你需要使用兩階段提交協議——prepare, commit/rollback,比如Java 7 的XAResource,還有MySQL 5.7的 XA Transaction,有些cache也支援XA,比如EhCache,關於事務問題後續再分析。

相關文章