Cache Design Pattern
當應用程式開始變慢時,原因可能是執行鏈中的某個環節出現了瓶頸。 有時候,這個瓶頸是由於錯誤造成的。有時候,是因為沒有設置最佳配置。有時候,資料提取的過程是瓶頸所在。 一種解決方法是改變整個架構。但在採取這種極端且可能昂貴的措施之前, 可以考慮一種權衡方案:不必每次都從遠程獲取資料,而是在第一次讀取後將資料存儲在本地。這就是緩存所提供的權衡:過時的資料與速度。
決定使用緩存只是漫長旅程的第一步。下一步是考慮應用程式和緩存之間的互動方式。本文將重點關注關於這些互動的選項。
Cache Aside
如果從緩存中找到數據,稱之為命中緩存,如果沒有找到數據,稱為之未命中緩存。 Cache-Aside 應該是使用最為廣泛的一種模式。應用直接去緩存中找數據,命中緩存則直接返回,如果未命中緩存,則需要先去資料庫中查詢數據,並將查詢到的數據存儲到緩存中。
- 應用程式首先檢查緩存。
- 如果在緩存中找到資料,則表示緩存命中。資料被讀取並返回給客戶端。
- 如果在緩存中找不到資料,則表示緩存未命中。應用程式需要進行一些額外的工作。它從資料庫查詢資料,將其返回給客戶端,並將資料存儲在緩存中,以便後續對相同資料的讀取可以命中緩存。
這種方式會讓緩存中的數據與資料庫中的數據不一致,所以一般會給緩存中的數據設置過期時間(TTL),數據過期之後就去資料庫中取最新的數據。 在數據更新時,應該和下文的 Write-Around 配合,而不要使用 Write-Through。
使用情境、優點和缺點:
Cache-aside 緩存通常是通用的,最適合讀取密集的工作負載(read-heavy workloads)。Memcached 和 Redis 是廣泛使用的工具。 使用 cache-aside 的系統對於緩存故障具有彈性(resilient to cache failures)。 如果緩存集群發生故障,系統仍然可以通過直接訪問資料庫來運行。(儘管在高峰負載期間緩存故障並不大有幫助。響應時間可能變得糟糕,最壞的情況是資料庫停止工作。)
另一個好處是緩存中的資料模型可以與資料庫中的資料模型不同。例如,作為多個查詢結果生成的回應可以存儲在某個請求 ID 下。
在使用 cache-aside 時,最常見的寫入策略是直接將資料寫入資料庫。在這種情況下,緩存可能與資料庫不一致。為了解決這個問題,開發人員通常使用生存時間(TTL),並在 TTL 過期之前繼續提供過期的資料。如果必須保證資料的新鮮度,開發人員要麼使緩存失效,要麼使用適當的寫入策略,這將在後續進行探討。
Read Through
Read-through 緩存位於與資料庫之間。當緩存未命中時,它會從資料庫中載入缺失的資料,填充緩存並返回給應用程式。 Read-Through 的方式與 Cache-Aside 的方式很接近.區別在於,Cache-Aside 是通過應用程式來更新緩存中的數據,而 Read-Through 則是通過緩存自身來更新數據,也就是說應用和資料庫之間不直接進行連接。
無論是 cache-aside 還是 read-through 策略,都是在第一次讀取時才將資料進行懶加載(lazy loading)。
使用情境、優點和缺點:
儘管 read-through 和 cache-aside 非常相似,但至少有兩個關鍵差異:
- 在 cache-aside 中,應用程式負責從資料庫中獲取資料並填充緩存。而在 read-through 中,這個邏輯通常由庫或獨立的緩存提供者支援。
- 與 cache-aside 不同,read-through 緩存的資料模型不能與資料庫不同。
對於讀取密集的工作負載(read-heavy workloads)且需要多次請求相同資料的情況,read-through 緩存效果最好,例如新聞報導。 缺點是當第一次請求資料時,總是會造成緩存未命中並產生額外的負擔以將資料加載到緩存中。 開發人員通常通過手動發出查詢來 "預熱" 或 "預加熱" 緩存來處理這個問題。 就像 cache-aside 一樣,資料在緩存和資料庫之間可能不一致,解決方案在於寫入策略,接下來我們將看到該如何處理。
這種模式也存在緩存中數據與資料庫中數據不一致的情況,但是相比於給緩存設置過期時間,它只需要和 Write-Through 搭配使用就可以解決這個問題。
Write Through
在這種寫入策略中,資料首先寫入緩存,然後再寫入資料庫。 緩存位於與資料庫之間,寫入操作始終通過緩存到達主要資料庫。這有助於緩存保持與主要資料庫的一致性。
當應用程式想要寫入資料或更新值時:
- 應用程式直接將資料寫入緩存。
- 緩存更新主要資料庫中的資料。當寫入完成後,緩存和資料庫都具有相同的值,並且緩存始終保持一致。
在Write Through策略中,先寫道快取,再寫到主記憶體。這保證了資料的一致性,因為主記憶體和快取中的資料始終保持一致。 優點:讀取操作非常快速,因為大部分資料都可以在快取中找到。此外,寫入操作也很快,因為資料會立即被寫入快取和主記憶體。 缺點:相較於其他策略,寫入操作的延遲可能較高,因為必須等待資料被寫入主記憶體。此外,如果在寫入之前發生故障,可能會導致資料不一致的問題。
使用情境、優點和缺點:
僅僅使用寫入透過緩存的方式似乎沒有太多作用,事實上,它引入了額外的寫入延遲(latency),因為資料首先寫入緩存,然後再寫入主要資料庫(兩個寫入操作)。 但是,當與讀取透過緩存結合使用時,我們可以獲得讀取透過緩存的所有好處,同時還可以獲得資料一致性的保證,從而不需要使用緩存失效(假設所有寫入操作都通過緩存進行到資料庫)。
DynamoDB Accelerator (DAX) 是一個很好的 read-through / write-through cache的例子。它位於 DynamoDB 和應用程式之間。對 DynamoDB 的讀寫可以通過 DAX 進行。(附註:如果您打算使用 DAX,請確保您熟悉其data consistency model以及它與 DynamoDB 的交互作用。)

Write Around
在Write Around策略中,寫入操作直接跳過快取,直接寫入主記憶體。這意味著資料不會存儲在快取中,而是直接存儲在主記憶體中。

使用情境、優點和缺點:
寫入繞過(write-around可以與讀取透過(read-through)結合,在數據僅寫入一次且較少或從不讀取的情況下提供良好的性能,例如即時日誌或聊天室訊息。同樣地,這種模式也可以與緩存分離(cache-aside)結合使用。
優點:
適用於數據寫入一次且較少讀取的場景, 可以減少緩存中的存儲需求, 寫入操作不會對快取造成壓力,快取中的資料保持相對較少,這可以避免快取充斥過多寫入的資料。 適合處理即時日誌等需要快速寫入但較少讀取的數據。 無需擔心緩存與數據庫之間的一致性問題。
缺點:
對於需要頻繁讀取的數據,可能無法發揮緩存的效益。 由於數據不經過緩存,讀取操作可能會帶來較高的延遲。 如果寫入的數據後續被讀取,可能會導致緩存未命中,需要從數據庫中讀取,增加讀取延遲。
Write Back or Write Behind
Write-Back 算是 Write-Through 的改良版,Write-Through 每寫一次緩存,緩存就會寫一次資料庫,而 Write-Back 則是寫了多次緩存後才會寫一次資料庫,異步方式更新,可以大大減輕伺服器的壓力。 從應用程式的角度來看,寫入到寫回緩存的速度更快,因為只需要在返回響應之前更新緩存。 這一模式在 MySQL 等資料庫產品中使用很廣泛。
使用情境、優點和缺點:
寫回緩存(Write-Back Cache)提升了寫入性能,適用於寫入較多的工作負載。當與讀取透過結合使用時,對於混合工作負載非常有效,因為最近更新和訪問的數據始終可在緩存中獲取。
它對於資料庫故障具有強韌性,可以容忍一定的資料庫停機時間。 如果支援批次處理或合併操作,可以減少對資料庫的寫入次數,減輕負載並降低成本,特別是當資料庫提供者根據請求數量收費時,例如 DynamoDB。 請記住,DAX 是寫入透過(write-through),因此如果應用程式的寫入負載很重,你將不會看到任何成本降低。(When I first heard of DAX, this was my first question - DynamoDB can be very expensive, but damn you Amazon.)
一些開發人員使用 Redis 同時用於cache-aside 和 write-back,以更好地吸收高峰期的突然增加負載。主要的缺點是如果緩存失效,數據可能會永久丟失。
大多數關聯式資料庫存儲引擎(例如 InnoDB)在內部默認啟用 write-back cache。查詢首先被寫入記憶體,最終刷新到磁碟。
優點:相較於Write Through,Write Back策略減少了寫入到主記憶體的次數,從而提升了寫入操作的效能。 缺點:相較於Write Through,讀取操作的效能可能較低,因為如果需要的資料恰好在快取中被更新
Refresh-Ahead
常言道,在計算機科學中有兩件難事:命名事物和緩存失效(Cache Invalidation)。 緩存失效是關於如何規劃項目在緩存中存儲多長時間,然後過期。當項目過期或緩存仍然為空時,您需要使用上述的其中一種模式(緩存分離或讀取透過)從數據存儲區中檢索項目。
這兩種模式都實現了涉及代碼、緩存和數據存儲區的流程。如上所述,從數據存儲區讀取是一個昂貴的操作:您需要首先通過網絡並從數據存儲區請求數據。如果您可以預取數據,在您發出請求之前使其可用,從而避免在關鍵路徑上產生性能損失,那該多好?這正是 Refresh-Ahead 的作用。
Refresh-Ahead 的實現取決於緩存提供者。一個與提供者無關的安全選擇是使用 Hazelcast Jet。憑藉其 Change-Data-Capture(CDC)功能,Jet 可以連接到任何具有公共 API 的緩存提供者,並在數據存儲區更新後立即更新緩存實體。以下是 CDC 的簡要概述:
For more details on Refresh-Ahead, please check Designing an Evergreen Cache with Change-Data-Capture.
Allocate on write
Write-miss(就是你要更新的東西 不在緩存裡),在這種情況下. 客戶直接向緩存發寫需求沒問題. 更新了DB之後,這時DB就可以選擇同步(Synchronous) 或是不更新緩存. 同步的更新叫做 Allocate on write 不更新的叫做 Write around
當應用程序將數據寫入緩存時,緩存系統會為該數據分配一塊空間,並將數據存儲在這個空間中。 這種分配發生在實際寫入操作之前,確保了寫入操作的原子性。 因此,數據在被寫入緩存之前已經有了確定的空間,並且寫入操作可以直接在這個預先分配的位置進行,而不需要再進行後續的分配或重組。
使用情境、優點和缺點:
Allocate on write策略的優點是它可以確保寫入操作的高效性和原子性,因為在實際寫入之前就已經完成了空間分配。這樣可以減少寫入操作的延遲,並提高寫入的效率。
然而,Allocate on write策略的缺點是它可能會導致緩存空間的浪費。即使某些數據在後續操作中從未被訪問或更新,也會為其分配空間。這可能導致緩存的空間使用效率下降,並浪費了寶貴的緩存資源。
Summary
如果您選擇了錯誤的策略,與您的目標或訪問模式不匹配,會發生什麼情況?您可能會引入額外的延遲,或者至少無法完全享受到全部好處。
讀 緩存跟DB要: Read through 自己跟DB要: Read-aside
寫 當你要寫新東西,你就三種可能:
- 寫Cache -> Write back
- 寫DB -> Write around
- Cache, DB都寫 -> Write through或Allocate on write
例如,如果您選擇了 write-through/read-through 的策略,但實際上應該使用write-around/read-through的策略(寫入的數據較少被訪問. written data is accessed less frequently),則您的緩存中將包含無用的垃圾數據。或許,如果緩存足夠大,這可能沒有太大問題。但在許多現實世界中,高吞吐量的系統中,記憶體永遠不夠大且伺服器成本是一個關注點時,正確的策略至關重要。
| PATTERN | CONSIDER | CONS |
|---|---|---|
| Cache-Aside | When you’re limited by the capabilities of your cache provider | The application is responsible for the cache orchestration flow |
| Read-Through | Solid default | |
| Write-Through | Solid default | |
| Write-behind | When performance considerations outweigh short-term consistency | Asynchronous systems are harder to reason with |
| Refresh-ahead | When fetching data from the datastore impairs throughput | Additional component to develop, deploy and maintain |