在許多專案中,我注意到雖然快取可能很方便——尤其是在客戶端——但它經常被忽略。客戶端快取對於透過減少延遲和卸載重複的伺服器請求來增強使用者體驗至關重要。例如,在具有無限滾動或頻繁更新儀表板的應用程式中,快取先前獲取的資料可以防止不必要的 API 調用,從而確保更順暢的互動和更快的渲染時間。
在我最近的一個專案中,實作快取將 API 呼叫量減少了 40% 以上,從而顯著提高了效能並節省了成本。這強調了為什麼客戶端快取應被視為基本最佳化策略。快取往往是最後考慮的功能之一,儘管它透過相對簡單的實作對效能產生了重大影響,無論是由於開發時間限制還是其他優先事項。
快取可以在架構中的各個層級實現:從使用 Redis(用於靜態內容的 CDN)的後端緩存,到客戶端上的記憶體緩存,甚至使用 localStorage 或 IndexedDB 來實現持久性。理想情況下,這些策略應該結合起來,以減少資料庫和 API 的負載和成本,以及客戶端-伺服器請求的延遲,特別是對於先前已經取得的資料。
在本文中,我們將探索如何在 JavaScript 中設計和實現具有 TTL(生存時間)支援的 LRU(最近最少使用)緩存,創建一個類似於我的 adev-lru 的套件。最後,您將獲得一個工作範例,展示有效快取解決方案的核心原理和功能。
LRU(最近最少使用)快取可確保最近存取的項目保留在記憶體中,同時在超出其容量時驅逐最近最少存取的項目。此策略的工作原理是維護使用順序:每個附件都會更新該項目在快取中的位置,首先刪除訪問次數最少的項目。
與其他快取策略相比,LRU 平衡了簡單性和效率,非常適合最近使用情況是未來存取的可靠指標的場景。例如,快取 API 回應、縮圖或頻繁存取的使用者首選項的應用程式可以利用 LRU 來減少冗餘的擷取操作,而不會導致逐出流程過於複雜。
與追蹤存取頻率並需要額外記帳的 LFU(最不常用)不同,LRU 避免了這種複雜性,同時在許多實際用例中仍然實現了出色的效能。類似地,FIFO(先進先出)和 MRU(最近使用)提供替代驅逐策略,但可能與最近活動至關重要的使用模式不太一致。透過在我的實作中將 LRU 與 TTL(生存時間)支援相結合,它還可以處理資料需要自動過期的場景,進一步增強其在即時儀表板或串流媒體服務等動態環境中的適用性。它在訪問最新數據至關重要的應用程式中特別有用。
LRUCache 類別的建置是為了有效率、支援靈活的配置並處理自動驅逐。以下是一些關鍵方法:
public static getInstance<T>(capacity: number = 10): LRUCache<T> { if (LRUCache.instance == null) { LRUCache.instance = new LRUCache<T>(capacity); } return LRUCache.instance; }
此方法可確保應用程式中只有一個快取實例,這是一種簡化資源管理的設計選擇。透過將快取實作為單例,我們可以避免冗餘記憶體使用並確保整個應用程式中資料的一致性。這在多個元件或模組需要存取相同快取資料的場景中特別有價值,因為它可以防止衝突並確保同步,而無需額外的協調邏輯。如果沒有指定容量,則預設為 10。
public put(key: string, value: T, ttl: number = 60_000): LRUCache<T> { const now = Date.now(); let node = this.hash.get(key); if (node != null) { this.evict(node); } node = this.prepend(key, value, now + ttl); this.hash.set(key, node); if (this.hash.size > this.capacity) { const tailNode = this.pop(); if (tailNode != null) { this.hash.delete(tailNode.key); } } return this; }
此方法新增或更新快取中的項目。當某個鍵已經存在時,其對應的項將被逐出並重新加入到快取的前面。為此,快取使用雙向鍊錶將資料保存為節點,並保持從列表末尾(尾部)刪除資料並將其移動到列表開頭(頭)的能力,以保證常數O (1)讀取每個節點的數據哈希表用於保存指向鍊錶每個節點的指標。此過程與 LRU 原則保持一致,確保最近訪問的項目始終具有優先級,從而有效地將它們標記為「最近使用的」。透過這樣做,快取可以保持準確的使用順序,這對於在超出容量時做出驅逐決策至關重要。此行為可確保資源得到最佳管理,從而最大限度地縮短頻繁存取資料的檢索時間。如果該鍵已存在,則該項目將移至前面以將其標記為最近使用。
public get(key: string): T | undefined { const node = this.hash.get(key); const now = Date.now(); if (node == null || node.ttl < now) { return undefined; } this.evict(node); this.prepend(node.key, node.value, node.ttl); return node.value; }
此方法檢索儲存的項目。如果該項目已過期,則會從快取中刪除。
為了評估快取的效率,我實現了命中率、未命中率和逐出等效能指標:
public static getInstance<T>(capacity: number = 10): LRUCache<T> { if (LRUCache.instance == null) { LRUCache.instance = new LRUCache<T>(capacity); } return LRUCache.instance; }
public put(key: string, value: T, ttl: number = 60_000): LRUCache<T> { const now = Date.now(); let node = this.hash.get(key); if (node != null) { this.evict(node); } node = this.prepend(key, value, now + ttl); this.hash.set(key, node); if (this.hash.size > this.capacity) { const tailNode = this.pop(); if (tailNode != null) { this.hash.delete(tailNode.key); } } return this; }
此方法會清除所有項目並重置快取狀態。
在我的實作中,我還添加了其他方法,例如 getOption ,而不是返回 T | undefined 它為那些喜歡更實用的方法的人返回 monad Option 的實例。我還添加了一個 Writer monad 來追蹤快取上的每個操作以進行日誌記錄。
您可以在此儲存庫中看到演算法涉及的所有其他方法,並且評論非常好:https://github.com/Armando284/adev-lru
LRU 快取不是唯一的選擇。選擇正確的快取演算法在很大程度上取決於應用程式的特定要求和存取模式。以下是 LRU 與其他常用快取策略的比較以及何時使用每種策略的指南:
LRU 在簡單性和有效性之間取得了平衡,使其成為近期活動與未來使用密切相關的應用程式的理想選擇。例如:
相反,如果訪問模式顯示頻率或插入順序更相關,那麼 LFU 或 FIFO 等演算法可能是更好的選擇。評估這些權衡可確保快取策略符合應用程式的目標和資源限制。
實現記憶體快取可以顯著提高應用程式的效能,減少回應時間並改善使用者體驗。
如果您想查看完整的 LRU 緩存,可以使用我的 npm 套件 https://www.npmjs.com/package/adev-lru 我也很想得到您的反饋以不斷改進它。
嘗試該套餐並分享您的想法,或者如果您想提供更多幫助? !
以上是如何創建記憶體緩存的詳細內容。更多資訊請關注PHP中文網其他相關文章!