Bei vielen Projekten ist mir aufgefallen, dass ein Cache zwar praktisch sein kann – insbesondere auf der Clientseite –, aber oft übersehen wird. Clientseitiges Caching ist entscheidend für die Verbesserung des Benutzererlebnisses, indem es die Latenz reduziert und wiederholte Serveranfragen entlastet. Beispielsweise verhindert das Zwischenspeichern zuvor abgerufener Daten in Anwendungen mit unendlichem Scrollen oder häufig aktualisierten Dashboards unnötige API-Aufrufe und sorgt so für reibungslosere Interaktionen und schnellere Renderzeiten.
In einem meiner jüngsten Projekte reduzierte die Implementierung eines Caches das API-Aufrufvolumen um über 40 %, was zu spürbaren Leistungsverbesserungen und Kosteneinsparungen führte. Dies unterstreicht, warum clientseitiges Caching als grundlegende Optimierungsstrategie betrachtet werden sollte. Ein Cache ist tendenziell eine der letzten berücksichtigten Funktionen, obwohl er bei relativ einfacher Implementierung erhebliche Auswirkungen auf die Leistung hat, sei es aufgrund von Einschränkungen bei der Entwicklungszeit oder anderen Prioritäten.
Ein Cache kann auf verschiedenen Ebenen der Architektur implementiert werden: vom Backend-Caching mit Redis, einem CDN für statische Inhalte, bis hin zu einem In-Memory-Cache auf dem Client oder sogar der Verwendung von localStorage oder IndexedDB für Persistenz. Idealerweise sollten diese Strategien kombiniert werden, um die Belastung und Kosten von Datenbanken und APIs sowie die Verzögerung durch Client-Server-Anfragen zu reduzieren, insbesondere für Daten, die bereits zuvor abgerufen wurden.
In diesem Artikel erfahren Sie, wie Sie einen LRU-Cache (Least Recent Used) mit TTL-Unterstützung (Time-to-Live) in JavaScript entwerfen und implementieren und so ein Paket erstellen, das meinem adev-lru ähnelt. Am Ende verfügen Sie über ein funktionierendes Beispiel, das die Kernprinzipien und Funktionalität einer effektiven Caching-Lösung demonstriert.
Ein LRU-Cache (Least Recent Used) stellt sicher, dass die Elemente, auf die zuletzt zugegriffen wurde, im Speicher verbleiben, während die Elemente, auf die zuletzt zugegriffen wurde, gelöscht werden, wenn ihre Kapazität überschritten wird. Bei dieser Strategie wird eine Nutzungsreihenfolge eingehalten: Jedes Zubehör aktualisiert die Position des Elements im Cache, wobei die Elemente, auf die am wenigsten zugegriffen wird, zuerst entfernt werden.
Im Vergleich zu anderen Caching-Strategien bietet LRU ein ausgewogenes Verhältnis zwischen Einfachheit und Effizienz und eignet sich daher gut für Szenarien, in denen die aktuelle Nutzung ein zuverlässiger Indikator für den zukünftigen Zugriff ist. Beispielsweise können Anwendungen, die API-Antworten, Miniaturansichten oder häufig aufgerufene Benutzereinstellungen zwischenspeichern, LRU nutzen, um redundante Abrufvorgänge zu reduzieren, ohne den Löschvorgang übermäßig zu komplizieren.
Im Gegensatz zu LFU (Least Frequently Used), das die Zugriffshäufigkeit verfolgt und zusätzliche Buchhaltung erfordert, vermeidet LRU diese Komplexität und erzielt dennoch in vielen realen Anwendungsfällen eine hervorragende Leistung. In ähnlicher Weise bieten FIFO (First In, First Out) und MRU (Most Recent Used) alternative Räumungsrichtlinien, passen jedoch möglicherweise nicht so gut zu Nutzungsmustern, bei denen die jüngste Aktivität von entscheidender Bedeutung ist. Durch die Kombination von LRU mit TTL-Unterstützung (Time-to-Live) in meiner Implementierung werden auch Szenarien verarbeitet, in denen Daten automatisch ablaufen müssen, wodurch die Anwendbarkeit in dynamischen Umgebungen wie Live-Dashboards oder Streaming-Diensten weiter verbessert wird. Dies ist besonders nützlich bei Anwendungen, bei denen der Zugriff auf die neuesten Daten von entscheidender Bedeutung ist.
Die LRUCache-Klasse ist darauf ausgelegt, effizient zu sein, flexible Konfigurationen zu unterstützen und automatische Räumungen durchzuführen. Nachfolgend sind einige wichtige Methoden aufgeführt:
public static getInstance<T>(capacity: number = 10): LRUCache<T> { if (LRUCache.instance == null) { LRUCache.instance = new LRUCache<T>(capacity); } return LRUCache.instance; }
Diese Methode stellt sicher, dass es nur eine Instanz des Caches in der Anwendung gibt, eine Designwahl, die die Ressourcenverwaltung vereinfacht. Durch die Implementierung des Caches als Singleton vermeiden wir redundante Speichernutzung und stellen konsistente Daten in der gesamten Anwendung sicher. Dies ist besonders wertvoll in Szenarien, in denen mehrere Komponenten oder Module Zugriff auf dieselben zwischengespeicherten Daten benötigen, da es Konflikte verhindert und die Synchronisierung gewährleistet, ohne dass zusätzliche Koordinationslogik erforderlich ist. Wenn keine Kapazität angegeben ist, wird standardmäßig 10 verwendet.
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; }
Diese Methode fügt ein Element im Cache hinzu oder aktualisiert es. Wenn ein Schlüssel bereits vorhanden ist, wird das entsprechende Element entfernt und vorne im Cache wieder hinzugefügt. Zu diesem Zweck verwendet der Cache eine doppelt verknüpfte Liste, um die Daten als Knoten zu speichern und die Möglichkeit beizubehalten, Daten vom Ende der Liste (Ende) zu löschen und an den Anfang der Liste (Kopf) zu verschieben, um ein konstantes O zu gewährleisten (1) Beim Lesen der Daten jedes Knotens wird eine Hash-Tabelle verwendet, um einen Zeiger auf jeden Knoten der Liste zu speichern. Dieser Prozess steht im Einklang mit dem LRU-Prinzip, indem sichergestellt wird, dass kürzlich aufgerufene Elemente immer priorisiert werden und sie effektiv als „zuletzt verwendet“ gekennzeichnet werden. Auf diese Weise behält der Cache eine genaue Nutzungsreihenfolge bei, die für die Räumungsentscheidung bei Überschreitung der Kapazität von entscheidender Bedeutung ist. Dieses Verhalten gewährleistet eine optimale Ressourcenverwaltung und minimiert die Abrufzeit für häufig aufgerufene Daten. Wenn der Schlüssel bereits vorhanden ist, wird das Element nach vorne verschoben, um es als zuletzt verwendet zu markieren.
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; }
Diese Methode ruft gespeicherte Elemente ab. Wenn der Artikel abgelaufen ist, wird er aus dem Cache entfernt.
Um die Effizienz des Caches zu bewerten, habe ich Leistungsmetriken wie Trefferquote, Fehlschläge und Räumungen implementiert:
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; }
Diese Methode löscht alle Elemente und setzt den Cache-Status zurück.
In meiner Implementierung habe ich auch andere Methoden wie getOption hinzugefügt, die statt T | zurückzugeben undefiniert Es gibt eine Instanz der Monad-Option zurück für diejenigen, die einen funktionaleren Ansatz bevorzugen. Ich habe außerdem eine Writer-Monade hinzugefügt, um jeden Vorgang im Cache zu Protokollierungszwecken zu verfolgen.
Sie können alle anderen an diesem Algorithmus beteiligten Methoden, sehr gut kommentiert, in diesem Repository sehen: https://github.com/Armando284/adev-lru
Ein LRU-Cache ist nicht die einzige Option. Die Wahl des richtigen Caching-Algorithmus hängt stark von den spezifischen Anforderungen und Zugriffsmustern der Anwendung ab. Nachfolgend finden Sie einen Vergleich von LRU mit anderen häufig verwendeten Caching-Strategien und Hinweise, wann diese jeweils zu verwenden sind:
LRU schafft ein Gleichgewicht zwischen Einfachheit und Effektivität und eignet sich daher ideal für Anwendungen, bei denen die jüngste Aktivität stark mit der zukünftigen Nutzung korreliert. Zum Beispiel:
Wenn dagegen Zugriffsmuster zeigen, dass Häufigkeit oder Einfügungsreihenfolge relevanter sind, sind Algorithmen wie LFU oder FIFO möglicherweise die bessere Wahl. Durch die Bewertung dieser Kompromisse wird sichergestellt, dass die Caching-Strategie mit den Zielen und Ressourcenbeschränkungen Ihrer Anwendung übereinstimmt.
Die Implementierung eines In-Memory-Cache kann die Leistung einer Anwendung erheblich steigern, Reaktionszeiten verkürzen und die Benutzererfahrung verbessern.
Wenn Sie einen vollständigen LRU-Cache in Aktion sehen möchten, können Sie mein npm-Paket https://www.npmjs.com/package/adev-lru verwenden. Ich würde mich auch über Ihr Feedback freuen, um es weiter zu verbessern.
Probieren Sie das Paket aus und teilen Sie Ihre Gedanken mit oder tragen Sie bei, wenn Sie Lust haben, mehr zu helfen?!
Das obige ist der detaillierte Inhalt vonSo erstellen Sie einen In-Memory-Cache. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!