"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.
"The two hardest things in Computer Science are cache invalidation, naming things, and off-by-one errors."
Réponses:
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
,b
versc
où, sia
etb
sont identiques, alorsc
c'est la même chose mais le coût de la vérificationb
est élevé, alors vous:b
b
plus rapidement possibleVous ne pouvez pas avoir votre gâteau et le manger ...
Si vous pouvez
a
superposer 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 deb
. Si vous avez choisi 2, vous devez toujours vérifier àb
chaque fois, mais vous pouvez vous rabattre sur le cache ena
cas deb
vé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
a
toujours une validité,b
alors vous pouvez organiser votre cache comme ceci (pseudocode):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 à laa
:b
relationx
:b
etx
: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
la source
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.
la source
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.
la source
À 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
Cell
s représentent un objet / une entité mis en cache et unCell
est 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
Cell
s 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'écrivainWriter (Set (uuid)) b
oùSet (uuid)
(notation Haskell) contient tous les identificateurs des valeurs mutables dontb
dépend la valeur calculée . Donc,uuid
est une sorte d'identifiant unique qui identifie la valeur / variable mutable (par exemple une ligne dans une base de données) dontb
dé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 defilter
) prennent les monades et(uuid, a)
-s Writer comme entrées, oùa
est une donnée / variable mutable, identifiée paruuid
.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épendb
la valeur calculée de type,b
vous pouvez invalider le cache qui contientb
si vous modifiez une valeura
dontb
dépend la valeur calculée , parce que sur la baseSet (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 valeursb
qui dépendent de la valeur mutable identifiée avec saiduuid
car la monade Writer dans laquelle leb
est enveloppé peut dire si celab
dépend de dituuid
ou 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.
la source
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.
la source
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
getData
ettransformData
.la source
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:
getData()
préféreraient un push afin d'éviter que la source ne soit inondée par une fonction getTimestampRemarque: 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.
la source
le cache est difficile car vous devez prendre en compte: 1) le cache est composé de plusieurs nœuds, il faut un consensus pour eux 2) le temps d'invalidation 3) la condition de concurrence lorsque plusieurs get / set se produisent
c'est une bonne lecture: https://www.confluent.io/blog/turning-the-database-inside-out-with-apache-samza/
la source
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
la source