快取是增強分散式應用程式效能和可擴充套件性的關鍵技術。這篇文章“掌握分散式應用程式中的快取”全面概述了高階快取技術和策略。
在大規模分散式應用程式中快取很難,團隊經常會經歷一個迭代和實驗的過程來調整他們的快取策略和實現,直到希望在某個時候,他們能夠將其調整到某種合理且半最佳的狀態。
在本文中,我想揭開並澄清一些經常被忽視或誤解的快取方面的問題。
希望閱讀本文後,您能夠更清楚地瞭解什麼是快取、快取的主要方法、需要注意的事項以及各種快取技術在實際用例中的應用。
什麼是快取?
簡而言之,快取是將資料儲存在臨時介質中的行為,與從原始儲存(記錄系統)中檢索資料相比,這種臨時介質更便宜、更快或更易於檢索。
訂單管理系統需要從庫存系統中檢索產品資訊。假設庫存系統效能不佳。每次收到請求時,它都必須去中央資料庫獲取產品資訊。該資料庫速度很慢,無法支援過多的並行請求。
為了提高效能並減輕庫存資料庫的壓力,我們引入了一個快取層,現在我們將在其中儲存相同的產品資訊。只是現在,我們不再使用笨重的資料庫來訪問庫存系統,而是先訪問快取,如果資料在快取中,我們就從那裡獲取資料。
我們在這裡所做的是引入一種臨時儲存介質(快取),以提高效能並最佳化原始清單資料庫的資源使用。
我們軟體開發領域的大多數人在聽到 "快取 "一詞時都會有非常具體的聯想。我們通常會將其與分散式快取產品聯絡起來,如 Redis、Memcached 或 EHCache。在其他時候,我們會想到瀏覽器快取、資料庫快取、作業系統快取,甚至硬體快取。這正是問題的關鍵所在。
快取的概念並不侷限於電腦科學領域的某一特定產品或領域。從最廣泛的意義上講,"快取 "實際上是我們從某個記錄系統中複製資料的任何型別的臨時介質。我們之所以這樣做,是因為將資料儲存在臨時介質中在某種程度上是有利的。
如果我們看一下前面的訂單管理和庫存系統的例子,快取層理論上可以是很多東西:
- 分散式快取產品(例如 Redis)
- 另一個擁有自己資料庫的微服務
- 實際庫存管理系統中的記憶體儲存
以上所有方式都符合快取的標準,儘管每種方式的實現都不盡相同。
簡單地說,以上所有這些都可以成為快取。快取,作為一個概念,可以而且事實上已經在計算機系統堆疊的各個層面和許多數字領域中實現了。
在我們繼續之前,瞭解有關快取主題的不同術語非常重要。
- 記錄系統:儲存資料的永久儲存。很可能是資料庫。也稱為真實來源系統。
- 快取未命中:當應用程式查詢快取但該特定記錄在快取中不存在時。
- 快取命中:當記錄確實存在於快取中並按原樣返回時。
- 快取汙染:當快取中填充了未使用或未查詢的值時。
- 快取驅逐:從快取中刪除條目以釋放記憶體的過程。
- 資料新鮮度:快取中的記錄與底層記錄系統的同步程度。
- 快取過期:作為驅逐過程的一部分或作為快取失效的一部分,基於時間刪除快取記錄,我們將在下面討論。
快取型別:
- 記憶體快取 (RAM):將經常訪問的資料儲存在記憶體中,以實現閃電般的快速訪問。
- 磁碟快取:將更大的資料集儲存在裝置的本地儲存器中,並在應用程式會話期間持久儲存。
- 網路快取:將資料儲存在遠端伺服器或 CDN 上,對於不經常變化的動態內容很有用。
快取失效策略:
- 基於時間的失效:根據預定義的生存時間 (TTL) 逐出快取資料。
- 基於訪問的無效:根據訪問模式或使用頻率逐出快取資料。
- 基於令牌的失效:使用令牌控制對快取資料的訪問,增強安全性。
有五種主要的快取模式,它們都與快取的讀取、寫入和與底層記錄系統同步的方式有關。
1、Cache-Aside 快取策略
Cache-Aside 快取策略可能是最流行的,也是大多數軟體工程師最熟悉的。這種快取方法將快取寫入和讀取的控制權完全交給應用程式。在這裡,應用程式既控制何時從資料庫或快取讀取,又控制何時寫入。
下面透過示例來詳細說明其工作原理。
想象一下,您的應用程式收到使用者的登入請求,並隨後獲取使用者的郵寄地址。
- 應用程式首先檢查使用者的地址是否存在於快取中。
- 如果該使用者沒有地址條目,應用程式將從資料庫中檢索資料。
- 然而,如果資訊存在於快取中,則會立即檢索該資料,從而節省我們訪問資料庫的時間。
- 當獲取到新的資訊之後,應用程式也會將該資料寫入快取中。
在步驟 2 中,如果快取中沒有該特定專案的條目 — — 這通常被稱為“快取未命中”。
優點
- 易於實現
- 控制權完全掌握在應用程式手中
- 使用最少的記憶體(至少在理論上),因為僅在需要時才獲取快取項(延遲載入)
缺點
- 由於必須從較慢的儲存中獲取資料,因此快取未命中延遲較高。快取未命中次數過多,效能可能會受到影響。
- 應用程式邏輯變得更加複雜(儘管整體思路很容易實現)
何時使用
- 當您想要完全控制快取的填充方式時。
- 當您沒有可以管理資料庫讀/寫的快取產品時。
- 當快取的訪問模式不規則時
2、直寫快取Write-Through
直寫快取可確保快取與底層持久資料儲存之間的一致性。換句話說,當發生寫入時,它會在同一事務中傳播到快取和資料庫。
舉個例子來說明一下:
- 財務應用程式收到更新使用者帳戶餘額的請求。
- 使用者賬戶餘額在資料庫和快取中都存在。
- 資料庫和快取都在同一事務中使用新值進行更新。
- 另一個請求來了,這次是讀取使用者的餘額。我們首先檢視快取並使用該值。由於快取具有最新的值,因此不必擔心該值可能與底層資料庫不同步。
請注意,步驟 3可以透過應用程式邏輯完成。但是,通常實際的快取產品將承擔這一責任。例如,如果您使用的是 EHCache 或 Infinispan,則應用程式將更新 Redis 快取,然後可以依次配置 Redis 快取以更新資料庫。
優點
- 確保快取和底層資料儲存之間的一致性
缺點
- 事務複雜性,因為我們現在需要某種兩階段提交邏輯來確保快取和資料庫都更新(如果不受快取控制)
- 操作複雜性,如果上述之一失敗,我們需要優雅地處理使用者體驗。
- 寫入變得更慢,因為我們現在需要更新兩個地方(快取和資料儲存),而不是隻更新一個地方(資料儲存)
何時使用
直寫式快取非常適合需要強資料一致性且無法提供過時資料的應用程式。它通常用於資料在寫入後必須立即保持準確和最新的環境中。
3、寫式快取 Write-Around
此策略會填充底層儲存,但不填充快取本身。換句話說,寫入會繞過快取,僅寫入底層儲存。此技術與Cache-Aside之間有一些重疊。
不同之處在於, Cache-Aside的重點是讀取和延遲載入 — 僅在首次從資料儲存中讀取資料時才將資料填充到快取中。而 Write-Around 快取的重點是寫入效能。當資料經常被寫入但不經常被讀取時,這種技術通常用於避免快取汙染。
優點
- 減少快取汙染,因為快取不會在每次寫入時填充
缺點
- 如果某些記錄經常被讀取,效能就會受到影響,因此最好主動將這些記錄載入到快取中,以防止第一次訪問資料庫。
何時使用
這通常在寫入量很大但讀取量明顯較低的情況下使用。
4、回寫式(Write-Behind)快取
寫入操作首先填充快取,然後寫入資料儲存區。這裡的關鍵是寫入資料儲存區是非同步進行的 — 因此無需兩階段事務提交。
Write-Behind 快取策略通常由快取產品處理。如果快取產品具有此機制,則應用程式將寫入快取,然後快取產品將負責將更改傳送到資料庫。如果快取產品不支援此功能,則應用程式本身將觸發對資料庫的非同步更新。
優點
- 寫入速度更快,因為系統只需在初始事務中寫入快取。資料庫將在稍後更新。
- 如果流程由快取產品處理,則應用程式邏輯就不會那麼複雜。
缺點
- 由於資料庫和快取在資料庫收到新的更改之前將不同步,因此可能會出現不一致的情況。
- 當快取最終嘗試更新資料庫時,可能會出現錯誤。如果發生這種情況,則需要更復雜的機制來確保資料庫收到最新的資料。
何時使用
當寫入效能至關重要時,可以使用後寫快取,資料庫中的資料暫時與快取略微不同步是可以接受的。它適用於寫入量大但一致性要求不太嚴格的應用程式。可以使用它的一個例子是 CDN(內容分發網路),用於快速更新快取內容,然後將其同步到記錄系統。
5、通讀read-through
從某種意義上說,直讀快取類似於快取旁路模式,因為在這兩種模式下,我們首先在快取中查詢記錄。如果發生快取未命中,我們會在資料庫中查詢。但是,雖然快取旁路模式將查詢快取和資料庫的責任放在應用程式上,但對於直讀,這個責任就落在了快取產品上(如果它有這種機制)
優點
- 簡單——所有邏輯都封裝在快取應用程式中
缺點
- 快取未命中時從資料庫讀取資料時可能會出現延遲。資料更新需要複雜的失效機制。
何時使用
當您想要簡化訪問資料的程式碼時,可以使用讀取快取。此外,當您想確保快取始終包含來自資料儲存的最新資料時,可以使用讀取快取。對於讀取資料比寫入資料更頻繁的應用程式來說,它很有用。但這裡的關鍵點是,您的快取產品應該能夠透過配置或本機從底層記錄系統執行這些讀取。
最難點:快取失效
現在我們瞭解了填充快取的不同方法,我們還需要了解如何使其與底層記錄系統保持同步。
說到快取失效,主要有兩種方法:基於時間的方法和基於事件的方法。基於時間的失效方法可以透過大多數快取產品中提供的生存時間 (TTL) 設定來控制。基於事件的方法需要應用程式或其他程式將新記錄傳送到快取。
資料快取的問題在於,它幾乎總是與底層資料儲存(記錄系統)至少略有不同步。換句話說,它會變得陳舊。為了使快取儘可能與記錄系統保持同步,我們需要實施某種快取失效策略。
換句話說,我們需要確保快取內的資料“新鮮度”。
快取失效會導致從記錄系統檢索新記錄並將其放入快取中。因此,瞭解快取失效與其與我們上面討論的快取策略之間的關係非常重要。
快取策略與如何從快取中載入和檢索資料有關。另一方面,快取失效則與記錄系統和快取之間的資料一致性和新鮮度有關。
因此,這兩個概念之間存在一些重疊,對於某些快取策略,失效會比其他策略更簡單。例如,使用快取寫入方法,快取會在每次寫入時更新,因此您無需額外實現這一點。但是,刪除可能不會反映出來,因此可能需要明確處理這些刪除的應用程式邏輯。
有兩種方法可以使快取條目無效:
1、事件驅動
使用事件驅動方法,您的應用程式會在底層記錄儲存發生更改時通知快取。每次記錄發生變化時,您都會觸發快取通知 — 無論是同步還是非同步。
這可以透過應用程式來完成,您的程式碼將負責保持快取最新。或者 — 對於某些快取產品,可能存在釋出/訂閱功能,快取產品可以訂閱這些型別的通知。在這種情況下,應用程式需要做的工作可能會減少。但是,仍然需要某些東西來生成這些通知事件。
2、基於時間
對於基於時間的方法,所有快取記錄都會有一個與之關聯的 TTL(生存時間)。記錄的 TTL 到期後,該快取記錄將被刪除。這通常由快取產品控制。
快取驅逐策略
快取驅逐與快取失效類似,因為在這兩種情況下,我們都會刪除舊的快取記錄。然而,兩者的區別在於,當快取已滿且無法容納更多記錄時,需要驅逐快取。
請記住,快取的目的是儲存最常訪問的記錄的子集。它不是複製整個事實來源系統。因此,快取的大小通常比儲存在資料庫/事實來源/記錄系統中的資料大小小几個數量級。
因此,我們需要一種可以“驅逐”或換句話說刪除記錄的機制。
同時,我們需要確保從應用程式最不可能需要的記錄開始 - 否則快取的全部意義就毫無意義了。
為了確保以最佳方式驅逐記錄,我們可以利用多種驅逐策略:
1、最近最少使用 (LRU)
透過這種方法,我們可以刪除一段時間內未使用的記錄。
何時使用:在資料被訪問的可能性隨著上次訪問記錄的時間而降低的情況下,這種方法非常有效。這種方法非常適合通用快取,因為訪問的近期性是未來訪問的有力指標。
何時不使用:對於資料訪問模式與新近度不相關的工作負載來說並不理想。
2、先進先出 (FIFO)
逐出那些在其他記錄之前儲存在快取中的記錄。
何時使用:適用於資料使用年限比訪問頻率或新近度更重要的快取。適合快取具有可預測壽命的資料。
何時不使用:對於仍可能頻繁訪問舊資料的工作負載來說,這不是最理想的。
3、最不常用(LFU)
逐出那些不經常使用/訪問的記錄。
何時使用:最適合需要長期保留頻繁訪問的資料的情況。適用於具有穩定訪問模式的應用程式。
何時不使用:在訪問模式可能發生顯著變化的環境中效果較差。不常訪問的專案可能會汙染快取。
4、生存時間 (TTL)
根據預定的離開期限進行驅逐。
何時使用:適用於在一定時期後變得過時或陳舊的資料。
何時不使用:不適用於其有效性不會隨著時間的推移而自然過期並且需要根據其他因素無限期地保留在快取中的資料。
5、隨機替換
隨機驅逐記錄。
何時使用:可用於複雜跟蹤機制的成本超過其收益的情況,或訪問模式不可預測且其他驅逐策略不太適合的情況。
何時不使用:在大多數實際場景中,當訪問模式或多或少可預測時,通常效率低於其他策略。