Cache-Invalidierung - Gibt es eine allgemeine Lösung?

118

"In der Informatik gibt es nur zwei schwierige Probleme: die Ungültigmachung des Caches und die Benennung von Dingen."

Phil Karlton

Gibt es eine allgemeine Lösung oder Methode zum Ungültigmachen eines Caches? Um zu wissen, wann ein Eintrag veraltet ist, damit Sie garantiert immer neue Daten erhalten?

Stellen Sie sich beispielsweise eine Funktion vor getData(), die Daten aus einer Datei abruft. Es speichert es basierend auf der letzten geänderten Zeit der Datei zwischen, die es bei jedem Aufruf überprüft.
Anschließend fügen Sie eine zweite Funktion hinzu, transformData()die die Daten transformiert und das Ergebnis für den nächsten Aufruf der Funktion zwischenspeichert. Die Datei ist nicht bekannt. Wie können Sie die Abhängigkeit hinzufügen, dass dieser Cache ungültig wird, wenn die Datei geändert wird?

Sie können getData()jedes Mal aufrufen, wenn transformData()es aufgerufen wird, und es mit dem Wert vergleichen, der zum Erstellen des Caches verwendet wurde. Dies kann jedoch sehr kostspielig werden.

Greg
quelle
6
Ich glaube, er hat etwas mit dem Schreiben von X Windows zu tun
Greg
1
Ich denke, dieser Titel wäre besser als "Cache Invalidation - Gibt es eine allgemeine Lösung?" da es sich auf eine bestimmte Klasse von Caching-Problemen bezieht.
RBarryYoung
71
Nein, er wusste nicht viel Informatik. Ich bin sicher, dass seine Beteiligung an der Erstellung von OpenGL, X11 und SSLv3 ihn zu beschäftigt gemacht hat, um es wirklich zu studieren. :-)
Tim Lesher
80
In der Informatik gibt es nur zwei schwierige Probleme: die Ungültigmachung des Caches. Dinge benennen. Und Fehler nacheinander.
Der Tag
8
Ich habe das einmal gehört als"The two hardest things in Computer Science are cache invalidation, naming things, and off-by-one errors."
Jonathon Reinhart

Antworten:

55

Sie sprechen von einer lebenslangen Abhängigkeitsverkettung, bei der eine Sache von einer anderen abhängig ist, die außerhalb ihrer Kontrolle geändert werden kann.

Wenn Sie eine idempotente Funktion von a, bnach cwo haben, wenn aund gleich bsind, dann csind sie gleich, aber die Kosten für die Überprüfung bsind hoch, dann haben Sie entweder:

  1. Akzeptieren Sie, dass Sie irgendwann mit veralteten Informationen arbeiten und nicht immer überprüfen b
  2. Geben Sie Ihr Bestes, um die Überprüfung bso schnell wie möglich zu gestalten

Sie können Ihren Kuchen nicht haben und ihn essen ...

Wenn Sie einen zusätzlichen Cache überlagern können, awirkt sich dies auf das anfängliche Problem nicht ein Bit aus. Wenn Sie 1 gewählt haben, haben Sie die Freiheit, die Sie sich selbst gegeben haben, und können somit mehr zwischenspeichern, müssen jedoch die Gültigkeit des zwischengespeicherten Werts von berücksichtigen b. Wenn Sie 2 gewählt haben, müssen Sie immer noch bjedes Mal nachsehen, können aber auf den Cache zurückgreifen, awenn Sie bauschecken.

Wenn Sie Caches überlagern, müssen Sie prüfen, ob Sie aufgrund des kombinierten Verhaltens gegen die 'Regeln' des Systems verstoßen haben.

Wenn Sie wissen, dass dies aimmer Gültigkeit bhat, können Sie Ihren Cache wie folgt anordnen (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;
}

Offensichtlich aufeinanderfolgende Schichtung (sagen wir x) ist trivial , so lange, in jeder Stufe die Gültigkeit des neu hinzugefügten Eingangseinstimmt a: bBeziehung x: bund x: a.

Es ist jedoch durchaus möglich, dass Sie drei Eingaben erhalten, deren Gültigkeit völlig unabhängig (oder zyklisch) war, sodass keine Überlagerung möglich wäre. Dies würde bedeuten, dass die mit // wichtig gekennzeichnete Zeile geändert werden muss

if (endCache [a] abgelaufen oder nicht vorhanden)

ShuggyCoUk
quelle
3
oder wenn die Kosten für die Überprüfung von b hoch sind, verwenden Sie pubsub, sodass c benachrichtigt wird, wenn sich b ändert. Das Beobachtermuster ist üblich.
user1031420
15

Das Problem bei der Cache-Ungültigmachung ist, dass sich Dinge ändern, ohne dass wir davon erfahren. In einigen Fällen ist eine Lösung möglich, wenn etwas anderes darüber Bescheid weiß und uns benachrichtigen kann. In dem angegebenen Beispiel könnte sich die Funktion getData in das Dateisystem einbinden, das alle Änderungen an Dateien kennt, unabhängig davon, welcher Prozess die Datei ändert, und diese Komponente könnte wiederum die Komponente benachrichtigen, die die Daten transformiert.

Ich glaube nicht, dass es eine allgemeine magische Lösung gibt, um das Problem zu lösen. In vielen praktischen Fällen besteht jedoch möglicherweise die Möglichkeit, einen auf "Polling" basierenden Ansatz in einen auf "Interrupt" basierenden Ansatz umzuwandeln, wodurch das Problem einfach behoben werden kann.

Der Tag
quelle
3

Wenn Sie jedes Mal, wenn Sie die Transformation durchführen, getData () erhalten, haben Sie den gesamten Vorteil des Caches beseitigt.

In Ihrem Beispiel scheint es eine Lösung zu sein, beim Generieren der transformierten Daten auch den Dateinamen und die letzte Änderungszeit der Datei zu speichern, aus der die Daten generiert wurden (Sie haben dies bereits in der von getData zurückgegebenen Datenstruktur gespeichert). ), kopieren Sie also einfach diesen Datensatz in die von transformData ()) zurückgegebene Datenstruktur und überprüfen beim erneuten Aufruf von transformData () die zuletzt geänderte Zeit der Datei.

Alex319
quelle
3

IMHO, Functional Reactive Programming (FRP) ist in gewissem Sinne eine allgemeine Methode zur Lösung der Cache-Ungültigkeit.

Hier ist der Grund: Veraltete Daten in der FRP-Terminologie werden als Panne bezeichnet . Eines der Ziele von FRP ist es, das Fehlen von Störungen zu gewährleisten.

FRP wird in diesem 'Essence of FRP'-Vortrag und in dieser SO-Antwort ausführlicher erläutert .

Im Vortrag stellen die Cells ein zwischengespeichertes Objekt / eine zwischengespeicherte Entität dar und a Cellwird aktualisiert, wenn eine ihrer Abhängigkeiten aktualisiert wird.

FRP verbirgt den mit dem Abhängigkeitsdiagramm verknüpften Installationscode und stellt sicher, dass keine veralteten Cells vorhanden sind.


Eine andere Möglichkeit (anders als FRP), die ich mir vorstellen kann, besteht darin, den berechneten Wert (vom Typ b) in eine Art Schreibmonade zu verpacken, Writer (Set (uuid)) bin der Set (uuid)(Haskell-Notation) alle Bezeichner der veränderlichen Werte enthält, von denen der berechnete Wert babhängt. Es handelt sich also uuidum eine Art eindeutigen Bezeichner, der den veränderlichen Wert / die veränderbare Variable (z. B. eine Zeile in einer Datenbank) identifiziert, von der der berechnete Wert babhängt.

Kombinieren Sie diese Idee mit Kombinatoren, die mit dieser Art von Writer-Monade arbeiten und die zu einer allgemeinen Lösung für die Ungültigmachung des Caches führen können, wenn Sie diese Kombinatoren nur zur Berechnung einer neuen verwenden b. Solche Kombinatoren (z. B. eine spezielle Version von filter) verwenden Writer-Monaden und (uuid, a)-s als Eingaben, wobei aes sich um veränderbare Daten / Variablen handelt, die durch gekennzeichnet sind uuid.

Jedes Mal, wenn Sie die "ursprünglichen" Daten (uuid, a)(z. B. die normalisierten Daten in einer Datenbank, aus der bberechnet wurde) ändern, von denen der berechnete Wert vom Typ babhängt, können Sie den enthaltenen Cache ungültig machen, bwenn Sie einen Wert mutieren, avon dem der berechnete bWert abhängt , weil anhand der Set (uuid)in der Writer-Monade können Sie erkennen, wann dies geschieht.

Jedes Mal, wenn Sie etwas mit einem bestimmten Wert mutieren uuid, senden Sie diese Mutation an alle Cache-s, und sie machen die Werte ungültig, die bvon dem mit dem genannten veränderlichen Wert abhängen, uuidda die Writer-Monade, in die der Wert eingeschlossen ist, erkennen bkann, ob dies bvom uuidoder abhängt nicht.

Das zahlt sich natürlich nur aus, wenn Sie viel öfter lesen als schreiben.


Ein dritter praktischer Ansatz besteht darin, materialisierte Ansichten in Datenbanken zu verwenden und sie als Cache zu verwenden. AFAIK zielen sie auch darauf ab, das Invalidierungsproblem zu lösen. Dies begrenzt natürlich die Operationen, die die veränderlichen Daten mit den abgeleiteten Daten verbinden.

jhegedus
quelle
2

Ich arbeite gerade an einem Ansatz, der auf PostSharp- und Memoizing-Funktionen basiert . Ich habe es an meinem Mentor vorbeigeführt, und er stimmt zu, dass es eine gute Implementierung von Caching auf inhaltsunabhängige Weise ist.

Jede Funktion kann mit einem Attribut markiert werden, das den Ablaufzeitraum angibt. Jede auf diese Weise markierte Funktion wird gespeichert und das Ergebnis im Cache gespeichert, wobei ein Hash des Funktionsaufrufs und der Parameter als Schlüssel verwendet werden. Ich verwende Velocity für das Backend, das die Verteilung der Cache-Daten übernimmt.

Chris McCall
quelle
1

Gibt es eine allgemeine Lösung oder Methode zum Erstellen eines Caches, um zu wissen, wann ein Eintrag veraltet ist, sodass Sie garantiert immer neue Daten erhalten?

Nein, da alle Daten unterschiedlich sind. Einige Daten können nach einer Minute "veraltet" sein, andere nach einer Stunde, und einige können für Tage oder Monate in Ordnung sein.

In Bezug auf Ihre speziellen Beispiel ist die einfachste Lösung , die eine ‚Cache - Überprüfung‘ Funktion für Dateien zu haben, die man von beiden nennen getDataund transformData.

DisgruntledGoat
quelle
1

Es gibt keine allgemeine Lösung, aber:

  • Ihr Cache kann als Proxy (Pull) fungieren. Angenommen, Ihr Cache kennt den Zeitstempel der letzten Ursprungsänderung. Wenn jemand anruft getData(), fragt der Cache den Ursprung nach dem Zeitstempel der letzten Änderung. Wenn dies der Fall ist, gibt er den Cache zurück, andernfalls aktualisiert er seinen Inhalt mit dem Quellstempel und gibt seinen Inhalt zurück. (Eine Variation besteht darin, dass der Client den Zeitstempel direkt auf die Anforderung sendet. Die Quelle würde nur dann Inhalt zurückgeben, wenn der Zeitstempel unterschiedlich ist.)

  • Sie können weiterhin einen Benachrichtigungsprozess (Push) verwenden. Der Cache beobachtet die Quelle. Wenn sich die Quelle ändert, sendet er eine Benachrichtigung an den Cache, die dann als "schmutzig" gekennzeichnet wird. Wenn jemand getData()den Cache aufruft, der zuerst auf die Quelle aktualisiert wird, entfernen Sie das "schmutzige" Flag. Geben Sie dann den Inhalt zurück.

Die Wahl hängt im Allgemeinen ab von:

  • Die Häufigkeit: Viele Anrufe getData()würden einen Push bevorzugen, um zu vermeiden, dass die Quelle von einer getTimestamp-Funktion überflutet wird
  • Ihr Zugriff auf die Quelle: Besitzen Sie das Quellmodell? Wenn nicht, können Sie wahrscheinlich keinen Benachrichtigungsprozess hinzufügen.

Hinweis: Da die Verwendung des Zeitstempels die traditionelle Arbeitsweise von http-Proxys ist, besteht ein anderer Ansatz darin, einen Hash des gespeicherten Inhalts freizugeben. Die einzige Möglichkeit, wie ich weiß, dass zwei Entitäten zusammen aktualisiert werden, besteht darin, dass ich Sie anrufe (Pull) oder mich anrufe (Push). Das ist alles.

Flavien Volken
quelle
-2

Möglicherweise sind Algorithmen ohne Cache am allgemeinsten (oder zumindest weniger abhängig von der Hardwarekonfiguration), da sie zuerst den schnellsten Cache verwenden und von dort aus fortfahren. Hier ist ein MIT-Vortrag dazu: Cache Oblivious Algorithms

CookieOfFortune
quelle
3
Ich denke, dass er nicht über Hardware-Caches spricht - er spricht über seinen getData () - Code mit einer Funktion, die die Daten, die er aus einer Datei erhalten hat, in den Speicher "zwischenspeichert".
Alex319