在许多项目中,我注意到虽然缓存可能很方便——尤其是在客户端——但它经常被忽视。客户端缓存对于通过减少延迟和卸载重复的服务器请求来增强用户体验至关重要。例如,在具有无限滚动或频繁更新仪表板的应用程序中,缓存先前获取的数据可以防止不必要的 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中文网其他相关文章!