動態

詳情 返回 返回

六大緩存(Caching)策略揭秘:延遲與複雜性的完美平衡 - 動態 詳情

引言

在為應用程序添加緩存時,首先需要考慮緩存策略。緩存策略決定了從緩存和底層存儲(如數據庫或服務)進行讀寫操作的方式。

從高層次來看,你需要決定在發生緩存未命中(cache miss)時,緩存是被動還是主動的。也就是説,當應用程序從緩存中查找一個值但該值不存在或已過期時,緩存策略會決定是由應用程序還是緩存本身從底層存儲中獲取數據。不同的緩存策略在延遲和複雜性之間存在不同的權衡,下面我們將逐一探討。

在這裏插入圖片描述

1. 緩存旁路(Cache-Aside Caching)

緩存旁路是最常見的緩存策略之一。當緩存命中(cache hit)時,數據訪問延遲主要由通信延遲決定,通常較小,因為緩存可以部署在靠近應用程序的緩存服務器上,甚至直接在應用程序的內存中。

但在緩存未命中時,緩存是被動存儲,由應用程序負責更新緩存。即緩存僅報告未命中,應用程序需要從底層存儲中獲取數據並更新緩存。

工作流程

如圖 1 所示,應用程序通過緩存鍵(cache key)從緩存中查找值。緩存鍵決定了應用程序需要的數據。

  • 如果鍵存在於緩存中,緩存返回與該鍵關聯的值,應用程序直接使用。
  • 如果鍵不存在或已過期(緩存未命中),應用程序需要處理這種情況。應用程序從底層存儲(通常是數據庫)查詢數據,並將結果存儲到緩存中。

例如,假設你正在緩存用户信息,並使用用户 ID 作為查找鍵。在緩存未命中的情況下,應用程序通過用户 ID 從數據庫查詢用户信息,將查詢結果轉換為適合緩存的格式(例如 JSON),然後以用户 ID 為鍵、用户信息為值更新緩存。

在這裏插入圖片描述

優點

緩存旁路之所以流行,是因為它易於實現。開發者可以輕鬆設置一個緩存服務器(如 Redis),用於緩存數據庫查詢或服務響應。緩存服務器是被動的,不需要了解底層數據庫的細節或數據的映射方式,所有緩存管理和數據轉換都由應用程序完成。

在許多場景中,緩存旁路是降低應用延遲的簡單有效方法。通過將最相關的數據存儲在靠近應用程序的緩存服務器中,可以隱藏數據庫訪問的延遲。

缺點

  • 數據一致性問題:如果有多個併發讀者同時查找同一鍵,應用程序需要協調併發緩存未命中的處理,否則可能導致多次數據庫訪問和緩存更新,進而造成後續緩存查詢返回不一致的值。
  • 事務支持缺失:由於緩存和數據庫互不瞭解,應用程序需要負責協調數據更新,因此無法提供事務支持。
  • 尾部延遲(tail latency):緩存未命中時,訪問延遲取決於數據庫的讀取延遲。雖然緩存命中時訪問很快,但未命中的情況會導致顯著的延遲,因此數據庫的地理位置延遲仍然很重要。

2. 讀穿緩存(Read-Through Caching)

與緩存旁路不同,讀穿緩存在緩存未命中時是主動的。當發生緩存未命中,讀穿緩存會自動從底層存儲中讀取鍵對應的值。延遲與緩存旁路類似,但底層存儲的檢索延遲是從緩存到存儲的延遲,而非從應用程序到存儲的延遲,這可能因部署架構而有所減少。

工作流程

如圖 2 所示,應用程序通過鍵從緩存中查找數據。如果緩存未命中,緩存會自動從數據庫中查詢該鍵的值,更新自身緩存,並將值返回給應用程序。從應用程序的角度看,緩存未命中是透明的,因為緩存始終返回鍵對應的值,無論是否發生未命中。

在這裏插入圖片描述

優點

  • 一致性保證:由於緩存負責協調數據庫讀取和更新,它可以在併發緩存未命中時保證一致性,併為應用程序提供事務支持。
  • 簡化應用程序邏輯:緩存接管了緩存管理,降低了應用程序的複雜性。

缺點

  • 實現複雜性:緩存需要能夠訪問底層存儲,並將數據庫查詢結果轉換為適合緩存的格式(例如將 SQL 查詢結果轉為 JSON)。這使得緩存與應用程序的數據模型和格式更緊密耦合。
  • 尾部延遲:與緩存旁路類似,緩存未命中時的延遲取決於數據庫訪問速度。不過,讀穿緩存可以通過預刷新(refresh-ahead)等技術異步更新緩存,在值過期前提前更新,從而隱藏數據庫訪問延遲。

3. 寫穿緩存(Write-Through Caching)

緩存旁路和讀穿緩存主要針對讀操作的緩存策略,但有時也需要緩存支持寫操作。在這種情況下,緩存提供了一個接口,允許應用程序更新鍵的值。對於緩存旁路,應用程序直接與底層存儲通信並更新緩存;而對於讀穿緩存,寫操作有兩種策略:寫穿緩存寫後緩存

寫穿緩存是一種策略,當緩存被更新時,會立即將更新傳播到底層存儲。寫操作的延遲主要由底層存儲的寫延遲決定,這可能較高。

工作流程

如圖 3 所示,應用程序通過緩存提供的接口更新鍵值對。緩存先更新自身狀態,然後同步更新數據庫,等待數據庫提交更新後才確認緩存更新。
在這裏插入圖片描述

優點

  • 數據同步:寫穿緩存旨在保持緩存和底層存儲的同步。
  • 事務支持:通過犧牲一些延遲,寫穿緩存可以提供事務保證,確保緩存和數據庫要麼都更新,要麼都不更新。

缺點

  • 複雜性:與讀穿緩存類似,緩存需要能夠連接數據庫,並將緩存值轉換為數據庫查詢。例如,如果緩存存儲的是 JSON 格式的用户數據,緩存需要將 JSON 轉換為數據庫更新。
  • 寫延遲高:寫操作的延遲等同於數據庫的提交延遲,可能較高。
  • 一致性風險:對於非事務性緩存,如果緩存更新成功但數據庫更新失敗,可能會導致緩存與數據庫不一致。

4. 寫後緩存(Write-Behind Caching)

與寫穿緩存不同,寫後緩存會立即更新緩存,但延遲更新底層存儲。緩存可能會接受多次更新,然後批量更新到數據庫。

工作流程

如下圖所示,緩存接受多個更新(例如三次緩存更新),然後再將這些更新批量寫入數據庫。
在這裏插入圖片描述

優點

  • 低寫延遲:由於底層存儲的更新是異步的,緩存可以立即確認寫操作,寫延遲較低。
  • 批量更新:通過批量處理更新,減少了對數據庫的訪問壓力。

缺點

  • 事務支持缺失:緩存無法保證緩存和數據庫的同步,可能導致數據不一致。
  • 數據丟失風險:如果緩存崩潰且未將更新寫入底層存儲,可能會丟失數據,降低數據的持久性。

5. 客户端緩存(Client-Side Caching)

客户端緩存將緩存置於應用程序的客户端層。儘管緩存服務器(如 Redis)使用內存緩存,但應用程序仍需通過網絡(如 Redis 協議)訪問緩存。

如果應用程序是運行在數據中心的服務器,緩存服務器是理想的選擇,因為數據中心內的網絡延遲低,且緩存複雜性由緩存服務器處理。然而,對於用户設備上的應用程序,最後一公里的延遲可能顯著影響用户體驗,因此客户端緩存非常有吸引力。

工作流程

客户端緩存將緩存直接置於應用程序內,通常結合讀穿緩存寫後緩存以獲得最佳延遲性能。客户端通常無法直接訪問數據庫,而是通過代理或 API 服務器間接訪問。

優點

  • 低延遲:讀寫操作都在客户端本地完成,延遲極低。
  • 適合簡單讀緩存:對於需要低延遲的場景(如本地優先的應用程序),客户端緩存是理想選擇。

缺點

  • 事務支持困難:由於數據庫訪問存在間接層和高延遲,事務支持難以實現。
  • 內存佔用:客户端緩存會增加應用程序的內存消耗,因為需要存儲緩存數據。

6. 分佈式緩存(Distributed Caching)

目前討論的緩存策略假設只有一個緩存實例,例如在應用程序內緩存或單一 Redis 服務器。然而,為了降低地理位置延遲或擴展工作負載,通常需要多個緩存副本。

分佈式緩存涉及多個獨立工作或組成集羣的緩存實例。分佈式緩存需要考慮數據分區複製

  • 分區:緩存數據分佈在不同節點上,避免每個節點存儲所有數據。
  • 複製:為實現高可用性和降低訪問延遲,數據分區可以在多個節點上覆制。

優點

  • 地理延遲降低:通過在不同地理位置部署緩存實例,減少訪問延遲。
  • 可擴展性:支持更大的工作負載。

缺點

  • 複雜性增加:分佈式緩存需要處理分區和複製的複雜性,可能引入一致性問題或額外的管理開銷。

總結

每種緩存策略在延遲和複雜性之間都有不同的權衡:

  • 緩存旁路:簡單易用,但需要應用程序管理緩存,可能導致一致性問題和尾部延遲。
  • 讀穿緩存:緩存主動處理未命中,降低應用程序複雜性,但需要更緊密的耦合。
  • 寫穿緩存:保持緩存與數據庫同步,但寫延遲高。
  • 寫後緩存:寫延遲低,但可能犧牲一致性和持久性。
  • 客户端緩存:適合低延遲場景,但增加內存佔用且事務支持困難。
  • 分佈式緩存:適合大規模、低延遲場景,但複雜性高。
參考資料:本文改編自 Pekka Enberg 的文章,結合國內技術社區的反饋整理而成。

擴展鏈接

葡萄城思否問答子站

user avatar lab4ai 頭像 gaozhipeng 頭像 yuxl01 頭像 congjunhua 頭像 hightopo 頭像 huaweiyun 頭像 qinwanzi 頭像 daguaisou 頭像 codepencil 頭像 tizuqiudexiangpica 頭像 xishui_5ac9a340a5484 頭像 nulidexiaocongmang 頭像
點贊 20 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.