Maison >interface Web >js tutoriel >Comment créer un cache en mémoire

Comment créer un cache en mémoire

Patricia Arquette
Patricia Arquetteoriginal
2024-12-27 02:37:09665parcourir

How to Create an In-Memory Cache

Dans de nombreux projets, j'ai remarqué que même si un cache peut être pratique, notamment du côté client, il est souvent négligé. La mise en cache côté client est essentielle pour améliorer l'expérience utilisateur en réduisant la latence et en déchargeant les requêtes répétées du serveur. Par exemple, dans les applications avec un défilement infini ou des tableaux de bord fréquemment mis à jour, la mise en cache des données précédemment récupérées évite les appels d'API inutiles, garantissant des interactions plus fluides et des temps de rendu plus rapides.

Dans l'un de mes projets récents, la mise en œuvre d'un cache a réduit le volume d'appels d'API de plus de 40 %, entraînant des améliorations notables des performances et des économies de coûts. Cela souligne pourquoi la mise en cache côté client doit être considérée comme une stratégie d’optimisation fondamentale. Un cache a tendance à être l'une des dernières fonctionnalités considérées, malgré son impact significatif sur les performances avec une mise en œuvre relativement simple, que ce soit en raison de contraintes de temps de développement ou d'autres priorités.

Un cache peut être implémenté à différents niveaux d'architecture : de la mise en cache backend utilisant Redis, un CDN pour le contenu statique, à un cache en mémoire sur le client ou même en utilisant localStorage ou IndexedDB pour la persistance. Idéalement, ces stratégies devraient être combinées pour réduire la charge et le coût des bases de données et des API, ainsi que le décalage des requêtes client-serveur, en particulier pour les données qui ont déjà été récupérées auparavant.

Dans cet article, nous explorerons comment concevoir et implémenter un cache LRU (Least Récemment Utilisé) avec prise en charge TTL (Time-to-Live) en JavaScript, en créant un package similaire à mon adev-lru. À la fin, vous disposerez d'un exemple fonctionnel qui présente les principes et fonctionnalités de base d'une solution de mise en cache efficace.


Qu'est-ce qu'un cache LRU ?

Un cache LRU (Least Récemment Utilisé) garantit que les éléments les plus récemment consultés restent en mémoire tout en expulsant les moins récemment consultés lorsque leur capacité est dépassée. Cette stratégie fonctionne en maintenant un ordre d'utilisation : chaque accessoire met à jour la position de l'élément dans le cache, les éléments les moins consultés étant supprimés en premier.

Par rapport à d'autres stratégies de mise en cache, LRU équilibre simplicité et efficacité, ce qui la rend bien adaptée aux scénarios dans lesquels l'utilisation récente est un indicateur fiable d'un accès futur. Par exemple, les applications qui mettent en cache les réponses API, les miniatures ou les préférences utilisateur fréquemment consultées peuvent exploiter LRU pour réduire les opérations de récupération redondantes sans trop compliquer le processus d'expulsion.

Contrairement au LFU (Least Frequency Used), qui suit la fréquence d'accès et nécessite une comptabilité supplémentaire, LRU évite cette complexité tout en atteignant d'excellentes performances dans de nombreux cas d'utilisation réels. De même, FIFO (First In, First Out) et MRU (Most Récemment Utilisé) proposent des politiques d'expulsion alternatives, mais peuvent ne pas s'aligner aussi bien sur les modèles d'utilisation où l'activité récente est critique. En combinant LRU avec la prise en charge TTL (Time-to-Live) dans mon implémentation, il gère également les scénarios dans lesquels les données nécessitent une expiration automatique, améliorant encore son applicabilité dans des environnements dynamiques tels que des tableaux de bord en direct ou des services de streaming. C’est particulièrement utile dans les applications où l’accès aux données les plus récentes est essentiel.

Mise en œuvre

La classe LRUCache est conçue pour être efficace, prendre en charge des configurations flexibles et gérer les expulsions automatiques. Vous trouverez ci-dessous quelques méthodes clés :

Création du cache

public static getInstance<T>(capacity: number = 10): LRUCache<T> {
    if (LRUCache.instance == null) {
        LRUCache.instance = new LRUCache<T>(capacity);
    }
    return LRUCache.instance;
}

Cette méthode garantit qu'il n'y a qu'une seule instance du cache dans l'application, un choix de conception qui simplifie la gestion des ressources. En implémentant le cache en tant que singleton, nous évitons l'utilisation redondante de la mémoire et garantissons la cohérence des données dans toute l'application. Ceci est particulièrement utile dans les scénarios où plusieurs composants ou modules doivent accéder aux mêmes données mises en cache, car cela évite les conflits et garantit la synchronisation sans nécessiter de logique de coordination supplémentaire. Si aucune capacité n'est spécifiée, la valeur par défaut est 10.

Ajouter un élément au cache

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;
}

Cette méthode ajoute ou met à jour un élément dans le cache. Lorsqu'une clé existe déjà, son élément correspondant est expulsé et réajouté au début du cache. Pour ce faire, le cache utilise une liste doublement liée pour enregistrer les données en tant que nœuds et conserver la possibilité de supprimer les données de la fin de la liste — Tail — et de les déplacer au début de la liste — Head —, pour garantir un O constant. (1) lecture des données de chaque nœud, une table de hachage est utilisée pour enregistrer un pointeur vers chaque nœud de la liste. Ce processus s'aligne sur le principe LRU en garantissant que les éléments récemment consultés sont toujours prioritaires, les marquant ainsi comme « les plus récemment utilisés ». Ce faisant, le cache maintient un ordre d’utilisation précis, ce qui est essentiel pour prendre des décisions d’expulsion lorsque la capacité est dépassée. Ce comportement garantit que les ressources sont gérées de manière optimale, minimisant ainsi le temps de récupération des données fréquemment consultées. Si la clé existe déjà, l'élément est déplacé vers l'avant pour le marquer comme récemment utilisé.

Récupérer un élément du cache

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;
}

Cette méthode récupère les éléments stockés. Si l'élément a expiré, il est supprimé du cache.

Mesures de performances

Pour évaluer l'efficacité du cache, j'ai implémenté des mesures de performances telles que le taux de réussite, les échecs et les expulsions :

public static getInstance<T>(capacity: number = 10): LRUCache<T> {
    if (LRUCache.instance == null) {
        LRUCache.instance = new LRUCache<T>(capacity);
    }
    return LRUCache.instance;
}

Vider le cache

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;
}

Cette méthode efface tous les éléments et réinitialise l'état du cache.

Dans mon implémentation, j'ai également ajouté d'autres méthodes comme getOption qui, au lieu de renvoyer T | undefined, il renvoie une instance de l'option monade pour ceux qui préfèrent une approche plus fonctionnelle. J'ai également ajouté une monade Writer pour suivre chaque opération sur le cache à des fins de journalisation.

Vous pouvez voir toutes les autres méthodes impliquées dans cet algorithme, très bien commentées, sur ce dépôt : https://github.com/Armando284/adev-lru


Comparaison des algorithmes de cache

Un cache LRU n'est pas la seule option. Le choix du bon algorithme de mise en cache dépend fortement des exigences spécifiques de l’application et des modèles d’accès. Vous trouverez ci-dessous une comparaison de LRU avec d'autres stratégies de mise en cache couramment utilisées et des conseils sur le moment d'utiliser chacune d'entre elles :

  • LFU (Least Fréquemment Utilisé) : Cet algorithme expulse les éléments qui sont consultés le moins de fois. Il est idéal pour les scénarios dans lesquels la fréquence d’utilisation dans le temps est un meilleur indicateur de l’accès futur que la récence. Cependant, cela nécessite généralement une comptabilité supplémentaire pour suivre le nombre d’accès, ce qui le rend plus complexe et plus coûteux en termes de calcul que LRU. Utilisez LFU pour des applications telles que les moteurs de recommandation ou les pipelines d'apprentissage automatique où les modèles d'utilisation historiques sont essentiels.
  • FIFO (First In, First Out) : Cette approche supprime les éléments les plus anciens, quelle que soit la fréquence d'accès à ceux-ci. Bien qu’il soit simple à mettre en œuvre, le FIFO peut ne pas convenir à la plupart des cas d’utilisation car il ne prend pas en compte les modèles d’utilisation. Il peut fonctionner pour les applications avec des flux de travail fixes et prévisibles, tels que la mise en cache de configurations statiques ou d'actifs préchargés.
  • MRU (Most Récemment Utilisé) : MRU expulse les éléments les plus récemment consultés, à l'opposé de LRU. Cette stratégie est la mieux adaptée aux situations dans lesquelles les données plus anciennes sont plus susceptibles d'être réutilisées, comme les systèmes de restauration ou certains types d'opérations d'annulation.

Quand utiliser LRU

LRU établit un équilibre entre simplicité et efficacité, ce qui le rend idéal pour les applications où l'activité récente est fortement corrélée à une utilisation future. Par exemple :

  • Mise en cache Web et API : LRU est bien adapté pour réduire les appels d'API redondants, en particulier dans des scénarios tels que les vues paginées, le défilement infini ou l'interrogation fréquente des données en direct.
  • Applications multimédia : mettez en cache les éléments récemment lus ou consultés, tels que des vidéos ou des images.
  • Gestion de l'état de l'interface utilisateur : stockez les états des composants récemment consultés pour améliorer les performances de rendu.

En revanche, si les modèles d'accès montrent que la fréquence ou l'ordre d'insertion sont plus pertinents, des algorithmes comme LFU ou FIFO pourraient être un meilleur choix. L'évaluation de ces compromis garantit que la stratégie de mise en cache s'aligne sur les objectifs et les contraintes de ressources de votre application.

  • LFU (Les moins fréquemment utilisés) : supprime les éléments les moins consultés en fonction de la fréquence.
  • FIFO (First In, First Out) : expulse l'élément le plus ancien, quelle que soit son utilisation récente.
  • MRU (Most Récemment Utilisé) : Supprime les éléments les plus récemment ajoutés, contrairement à LRU.

Conclusion

La mise en œuvre d'un cache en mémoire peut améliorer considérablement les performances d'une application, en réduisant les temps de réponse et en améliorant l'expérience utilisateur.

Si vous souhaitez voir un cache LRU complet en action, vous pouvez utiliser mon package npm https://www.npmjs.com/package/adev-lru. J'aimerais également recevoir vos commentaires pour continuer à l'améliorer.

Essayez le package et partagez vos réflexions ou contribuez si vous avez envie d'aider davantage ?!

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn