Invalidation du cache - Existe-t-il une solution générale?


118

"Il n'y a que deux problèmes difficiles en informatique: l'invalidation du cache et la dénomination des choses."

Phil Karlton

Existe-t-il une solution ou une méthode générale pour invalider un cache; savoir quand une entrée est périmée, vous êtes donc assuré de toujours obtenir des données fraîches?

Par exemple, considérons une fonction getData()qui obtient des données à partir d'un fichier. Il le met en cache en fonction de la dernière heure de modification du fichier, qu'il vérifie à chaque fois qu'il est appelé.
Ensuite, vous ajoutez une deuxième fonction transformData()qui transforme les données et met en cache son résultat pour la prochaine fois que la fonction est appelée. Il n'a aucune connaissance du fichier - comment ajouter la dépendance selon laquelle si le fichier est modifié, ce cache devient invalide?

Vous pouvez appeler getData()chaque fois qu'il transformData()est appelé et le comparer avec la valeur qui a été utilisée pour créer le cache, mais cela pourrait s'avérer très coûteux.


6
Je crois qu'il a quelque chose à voir avec l'écriture de X Windows
Greg

1
Je pense que ce titre serait mieux comme "Invalidation du cache - Y a-t-il une solution générale?" car il fait référence à une classe spécifique de problème de mise en cache.
RBarryYoung

71
Non, il ne connaissait pas beaucoup l'informatique. Je suis sûr que son implication dans la création d'OpenGL, X11 et SSLv3 l'a rendu trop occupé pour vraiment l'étudier. :-)
Tim Lesher

80
Il n'y a que 2 problèmes difficiles en informatique: l'invalidation du cache. Nommer les choses. Et des erreurs ponctuelles.
The Dag

8
J'ai entendu ça une fois comme"The two hardest things in Computer Science are cache invalidation, naming things, and off-by-one errors."
Jonathon Reinhart

Réponses:


55

Ce dont vous parlez est le chaînage de dépendances à vie, qu'une chose dépend d'une autre qui peut être modifiée en dehors de son contrôle.

Si vous avez une fonction idempotente de a, bvers coù, si aet bsont identiques, alors cc'est la même chose mais le coût de la vérification best élevé, alors vous:

  1. acceptez que vous travaillez parfois avec des informations obsolètes et ne vérifiez pas toujours b
  2. faites de votre mieux pour vérifier le bplus rapidement possible

Vous ne pouvez pas avoir votre gâteau et le manger ...

Si vous pouvez asuperposer un cache supplémentaire basé sur le dessus, cela n'affecte pas du tout le problème initial. Si vous avez choisi 1, vous avez la liberté que vous vous êtes donnée et pouvez donc mettre en cache davantage, mais vous devez vous rappeler de considérer la validité de la valeur mise en cache de b. Si vous avez choisi 2, vous devez toujours vérifier à bchaque fois, mais vous pouvez vous rabattre sur le cache en acas de bvérification.

Si vous couchez des caches, vous devez déterminer si vous avez enfreint les «règles» du système en raison du comportement combiné.

Si vous savez que cela a atoujours une validité, balors vous pouvez organiser votre cache comme ceci (pseudocode):

private map<b,map<a,c>> cache // 
private func realFunction    // (a,b) -> c

get(a, b) 
{
    c result;
    map<a,c> endCache;
    if (cache[b] expired or not present)
    {
        remove all b -> * entries in cache;   
        endCache = new map<a,c>();      
        add to cache b -> endCache;
    }
    else
    {
        endCache = cache[b];     
    }
    if (endCache[a] not present)     // important line
    {
        result = realFunction(a,b); 
        endCache[a] = result;
    }
    else   
    {
        result = endCache[a];
    }
    return result;
}

Il est évident que la superposition des couches successives ( par exemple x) est trivial tant que, à chaque étape la validité de l'entrée nouvellement ajoutée correspond à la a: brelation x: bet x: a.

Cependant, il est tout à fait possible que vous puissiez obtenir trois entrées dont la validité était entièrement indépendante (ou était cyclique), donc aucune superposition ne serait possible. Cela signifierait que la ligne marquée // important devrait être remplacée par

if (endCache [a] expiré ou absent )


3
ou peut-être, si le coût de la vérification de b est élevé, vous utilisez pubsub de sorte que lorsque b change, il notifie c. Le modèle Observer est courant.
user1031420

15

Le problème de l'invalidation du cache est que les choses changent sans que nous le sachions. Ainsi, dans certains cas, une solution est possible s'il y a autre chose qui en sait et peut nous en informer. Dans l'exemple donné, la fonction getData pourrait se connecter au système de fichiers, qui connaît toutes les modifications apportées aux fichiers, quel que soit le processus qui modifie le fichier, et ce composant à son tour pourrait notifier le composant qui transforme les données.

Je ne pense pas qu'il y ait de solution magique générale pour faire disparaître le problème. Mais dans de nombreux cas pratiques, il peut très bien y avoir des opportunités de transformer une approche basée sur le «sondage» en une approche basée sur une «interruption», ce qui peut simplement faire disparaître le problème.


3

Si vous envisagez d'utiliser getData () à chaque fois que vous effectuez la transformation, vous avez éliminé tout l'avantage du cache.

Pour votre exemple, il semble qu'une solution consisterait lorsque vous générez les données transformées, à stocker également le nom de fichier et l'heure de la dernière modification du fichier à partir duquel les données ont été générées (vous avez déjà stocké cela dans la structure de données renvoyée par getData ( ), il vous suffit donc de copier cet enregistrement dans la structure de données renvoyée par transformData ()), puis lorsque vous appelez à nouveau transformData (), vérifiez la dernière heure de modification du fichier.


3

À mon humble avis, la programmation réactive fonctionnelle (FRP) est en un sens un moyen général de résoudre l'invalidation du cache.

Voici pourquoi: les données périmées dans la terminologie FRP sont appelées un problème . L'un des objectifs de FRP est de garantir l'absence de problèmes.

Le PRF est expliqué plus en détail dans cet exposé sur «L'essence du PRF» et dans cette réponse SO .

Dans l' exposé, les Cells représentent un objet / une entité mis en cache et un Cellest actualisé si l'une de ses dépendances est actualisée.

FRP masque le code de plomberie associé au graphe de dépendances et s'assure qu'il n'y a aucun Cells périmé .


Une autre façon (différente de FRP) à laquelle je peux penser consiste à envelopper la valeur calculée (de type b) dans une sorte de Monad d'écrivain Writer (Set (uuid)) bSet (uuid)(notation Haskell) contient tous les identificateurs des valeurs mutables dont bdépend la valeur calculée . Donc, uuidest une sorte d'identifiant unique qui identifie la valeur / variable mutable (par exemple une ligne dans une base de données) dont bdépend le calcul .

Combinez cette idée avec des combinateurs qui fonctionnent sur ce type d'écrivain Monad et qui pourraient conduire à une sorte de solution générale d'invalidation de cache si vous n'utilisez ces combinateurs que pour calculer un nouveau b. De tels combinateurs (disons une version spéciale de filter) prennent les monades et (uuid, a)-s Writer comme entrées, où aest une donnée / variable mutable, identifiée par uuid.

Ainsi, chaque fois que vous modifiez les données "originales" (uuid, a)(disons les données normalisées dans une base de données à partir de laquelle dépend bla valeur calculée de type, bvous pouvez invalider le cache qui contient bsi vous modifiez une valeur adont bdépend la valeur calculée , parce que sur la base Set (uuid)de la Monade Writer, vous pouvez dire quand cela se produit.

Ainsi, chaque fois que vous mute quelque chose avec un donné uuid, vous diffusez cette mutation à tous les cache-s et ils invalident les valeurs bqui dépendent de la valeur mutable identifiée avec said uuidcar la monade Writer dans laquelle le best enveloppé peut dire si cela bdépend de dit uuidou ne pas.

Bien sûr, cela ne paie que si vous lisez beaucoup plus souvent que vous n'écrivez.


Une troisième approche, pratique, consiste à utiliser des vues matérialisées dans des bases de données et à les utiliser comme cache-s. AFAIK ils visent également à résoudre le problème d'invalidation. Ceci limite bien sûr les opérations qui connectent les données mutables aux données dérivées.


2

Je travaille actuellement sur une approche basée sur PostSharp et des fonctions de mémorisation . Je l'ai passé devant mon mentor, et il convient que c'est une bonne implémentation de la mise en cache d'une manière indépendante du contenu.

Chaque fonction peut être marquée avec un attribut qui spécifie sa période d'expiration. Chaque fonction marquée de cette manière est mémorisée et le résultat est stocké dans le cache, avec un hachage de l'appel de fonction et des paramètres utilisés comme clé. J'utilise Velocity pour le backend, qui gère la distribution des données du cache.


1

Existe-t-il une solution ou une méthode générale pour créer un cache, pour savoir quand une entrée est périmée, de sorte que vous êtes assuré de toujours obtenir des données fraîches?

Non, car toutes les données sont différentes. Certaines données peuvent être «obsolètes» après une minute, d'autres après une heure, et certaines peuvent être correctes pendant des jours ou des mois.

En ce qui concerne votre exemple spécifique, la solution la plus simple est d'avoir une fonction de «vérification du cache» pour les fichiers, que vous appelez à la fois à partir de getDataet transformData.


1

Il n'y a pas de solution générale mais:

  • Votre cache peut agir comme un proxy (pull). Supposons que votre cache connaisse l'horodatage de la dernière modification d'origine, lorsque quelqu'un appelle getData(), le cache demande l'origine de l'horodatage de la dernière modification, s'il est identique, il renvoie le cache, sinon il met à jour son contenu avec celui de la source et renvoie son contenu. (Une variante est que le client envoie directement l'horodatage sur la demande, la source ne renverra le contenu que si son horodatage est différent.)

  • Vous pouvez toujours utiliser un processus de notification (push), le cache observer la source, si la source change, il envoie une notification au cache qui est alors marqué comme "sale". Si quelqu'un appelle, getData()le cache sera d'abord mis à jour vers la source, supprimez le drapeau "sale"; puis renvoyez son contenu.

Le choix dépend généralement de:

  • La fréquence: de nombreux appels getData()préféreraient un push afin d'éviter que la source ne soit inondée par une fonction getTimestamp
  • Votre accès à la source: êtes-vous propriétaire du modèle source? Sinon, il y a de fortes chances que vous ne puissiez pas ajouter de processus de notification.

Remarque: comme l'utilisation de l'horodatage est le mode de fonctionnement traditionnel des proxys http, une autre approche consiste à partager un hachage du contenu stocké. Le seul moyen que je connaisse pour que 2 entités se mettent à jour ensemble est soit je vous appelle (pull) soit vous m'appelez ... (push) c'est tout.



-2

Peut-être que les algorithmes inconscients du cache seraient les plus généraux (ou du moins, moins dépendants de la configuration matérielle), car ils utiliseront d'abord le cache le plus rapide et passeront à partir de là. Voici une conférence du MIT à ce sujet: Algorithmes oublieux du cache


3
Je pense qu'il ne parle pas de caches matériels - il parle de son code getData () ayant une fonctionnalité qui "met en cache" les données qu'il a obtenues d'un fichier dans la mémoire.
Alex319
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.