"Il n'y a que deux problèmes difficiles en informatique: l'invalidation du cache et le nommage."
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, donc vous êtes assuré d'obtenir toujours des données fraîches?
Par exemple, considérons une fonction getData()
qui récupère les données d'un fichier. Il le met en cache en fonction de la dernière heure de modification du fichier, qu'il vérifie à chaque appel.
Ensuite, vous ajoutez une deuxième fonction transformData()
qui transforme les données et met en cache son résultat pour le prochain appel de la fonction. 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 que 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.
Ce dont vous parlez, c'est le chaînage des 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
à c
où, si a
et b
sont les mêmes alors c
est le même mais le coût de vérification de b
est élevé alors vous non plus:
b
b
soit aussi rapide que possibleVous ne pouvez pas avoir votre gâteau et le manger ...
Si vous pouvez superposer un cache supplémentaire basé sur a
par dessus, cela n'affecte pas le problème initial d'un bit. Si vous avez choisi 1, vous disposez de la liberté que vous vous êtes donnée et pouvez donc mettre en cache plus, mais vous devez vous rappeler de considérer la validité de la valeur mise en cache de b
. Si vous en avez choisi 2, vous devez toujours vérifier b
à chaque fois, mais vous pouvez recourir au cache pour a
si b
sort.
Si vous superposez des caches, vous devez déterminer si vous avez violé les "règles" du système en raison du comportement combiné.
Si vous savez que a
a toujours une validité si b
le fait, alors vous pouvez organiser votre cache comme ça (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 successive (par exemple x
) est triviale tant que, à chaque étape, la validité de la nouvelle entrée ajoutée correspond à la relation a
: b
pour x
: b
et x
: a
.
Cependant, il est tout à fait possible que vous puissiez obtenir trois entrées dont la validité était entièrement indépendante (ou cyclique), donc aucune superposition ne serait possible. Cela signifierait que la ligne marquée // important devrait changer en
if (endCache [a] expiré o non présent)
Le problème de l'invalidation du cache est que les choses changent sans que nous le sachions. Donc, dans certains cas, une solution est possible s'il y a quelque chose d'autre qui le 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 pourrait à son tour notifier le composant qui transforme les données.
Je ne pense pas qu'il existe de solution magique générale pour faire disparaître le problème. Mais dans de nombreux cas pratiques, il peut très bien être possible de transformer une approche basée sur le "sondage" en une approche basée sur "l'interruption", ce qui peut simplement faire disparaître le problème.
Si vous allez obtenir getData () à chaque fois que vous effectuez la transformation, alors vous avez éliminé tous les avantages du cache.
Pour votre exemple, il semble qu'une solution serait lorsque vous générez les données transformées, pour stocker également le nom de fichier et la dernière heure de 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 retourné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.
À mon humble avis, la programmation réactive fonctionnelle (FRP) est en quelque sorte 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 glitch . L'un des objectifs de FRP est de garantir l'absence de pépins.
FRP est expliqué plus en détail dans cette 'Essence of FRP' talk and in this SO answer .
Dans le talk les Cell
représentent un objet/entité mis en cache et un Cell
est actualisé si l'une de ses dépendances est actualisée.
FRP masque le code de plomberie associé au graphique de dépendance et s'assure qu'il n'y a pas de Cell
périmés.
Une autre façon (différente de FRP) à laquelle je peux penser est d'envelopper la valeur calculée (de type b
) dans une sorte d'écrivain Monad Writer (Set (uuid)) b
where Set (uuid)
( Notation Haskell) contient tous les identifiants des valeurs mutables dont dépend la valeur calculée b
. Ainsi, uuid
est une sorte d'identifiant unique qui identifie la valeur/variable mutable (disons une ligne dans une base de données) dont dépend le b
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 du cache si vous utilisez uniquement ces combinateurs pour calculer un nouveau b
. Ces combinateurs (disons une version spéciale de filter
) prennent en entrée les monades Writer et (uuid, a)
- s, où a
est une variable/donnée modifiable, 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 b
a été calculée) dont dépend la valeur calculée de type b
vous pouvez invalider le cache qui contient b
si vous mutez une valeur a
dont dépend la valeur b
calculée, car basée sur la Set (uuid)
dans le Writer Monad vous pouvez dire quand cela se produit.
Donc, à chaque fois que vous mutez quelque chose avec un uuid
donné, vous diffusez cette mutation à tous les cache-s et ils invalident les valeurs b
qui dépendent de la valeur mutable identifiée avec ledit uuid
car la monade Writer dans laquelle le b
est encapsulé peut dire si cela b
dépend dudit uuid
ou non.
Bien sûr, cela ne vaut que si vous lisez beaucoup plus souvent que vous n'écrivez.
Une troisième approche pratique consiste à utiliser des vues matérialisées dans les bases de données et à les utiliser comme cache-es. AFAIK ils visent également à résoudre le problème d'invalidation. Cela limite bien sûr les opérations qui connectent les données mutables aux données dérivées.
Je travaille actuellement sur une approche basée sur PostSharp et 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 ainsi marquée 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 de cache.
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é d'obtenir toujours de nouvelles données?
Non, car toutes les données sont différentes. Certaines données peuvent être "périmées" au bout d'une minute, d'autres au bout d'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 consiste à avoir une fonction de "vérification du cache" pour les fichiers, que vous appelez à la fois getData
et transformData
.
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 du dernier changement d'origine, lorsque quelqu'un appelle getData()
, le cache demande à l'origine son horodatage du dernier changement, s'il est le même, il renvoie le cache, sinon il met à jour son contenu avec celui de la source et retourner son contenu. (Une variante est que le client envoie directement l'horodatage sur la demande, la source ne retournerait du contenu que si son horodatage est différent.)
Vous pouvez toujours utiliser un processus de notification (Push), le cache observe 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 à la source, supprimez le drapeau "sale"; puis retournez son contenu.
Le choix dépend généralement:
getData()
préféreraient un Push donc pour éviter que la source ne soit inondée par une fonction getTimestampRemarque: Étant donné que l'utilisation de l'horodatage est le mode de fonctionnement traditionnel des proxy http, une autre approche consiste à partager un hachage du contenu stocké. La seule façon que je connaisse pour que 2 entités soient mises à jour ensemble est que je vous appelle (tirez) ou que vous m'appeliez ((poussez)).
le cache est difficile car vous devez prendre en compte: 1) le cache est constitué de plusieurs nœuds, nécessite 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/