Invalidazione della cache: esiste una soluzione generale?


118

"Ci sono solo due problemi difficili in Informatica: l'invalidazione della cache e la denominazione delle cose".

Phil Karlton

Esiste una soluzione o un metodo generale per invalidare una cache; sapere quando una voce è obsoleta, quindi hai sempre la garanzia di ottenere dati aggiornati?

Si consideri ad esempio una funzione getData()che ottiene dati da un file. Lo memorizza nella cache in base all'ora dell'ultima modifica del file, che controlla ogni volta che viene chiamato.
Quindi aggiungi una seconda funzione transformData()che trasforma i dati e memorizza nella cache il suo risultato per la prossima volta che la funzione viene chiamata. Non è a conoscenza del file: come si aggiunge la dipendenza che se il file viene modificato, questa cache diventa non valida?

È possibile chiamare getData()ogni volta che transformData()viene chiamato e confrontarlo con il valore utilizzato per creare la cache, ma potrebbe risultare molto costoso.


6
Credo che abbia qualcosa a che fare con la scrittura di X Windows
Greg

1
Penso che il titolo sarebbe meglio come "Invalidazione cache - Esiste una soluzione generale?" in quanto si riferisce a una classe specifica di problemi di memorizzazione nella cache.
RBarryYoung,

71
No, non conosceva molto l'informatica. Sono sicuro che il suo coinvolgimento nella creazione di OpenGL, X11 e SSLv3 lo ha reso troppo impegnato per studiarlo davvero molto. :-)
Tim Lesher

80
Ci sono solo 2 problemi difficili in informatica: l'invalidazione della cache. Nominare le cose. E errori off-by-one.
Il

8
Una volta l'ho sentito come"The two hardest things in Computer Science are cache invalidation, naming things, and off-by-one errors."
Jonathon Reinhart,

Risposte:


55

Quello di cui stai parlando è il concatenamento delle dipendenze a vita, che una cosa dipende da un'altra che può essere modificata al di fuori del suo controllo.

Se hai una funzione idempotente da a, ba cdove, se ae bsono uguali allora cè lo stesso ma il costo del controllo bè alto allora tu:

  1. accetta che a volte operi con informazioni non aggiornate e non le controlli sempre b
  2. fai del tuo meglio per controllare b più veloce possibile

Non puoi avere la tua torta e mangiarla ...

Se è possibile asovrapporre una cache aggiuntiva basata su over the top, ciò influirà sul problema iniziale non per un po '. Se scegli 1, hai la libertà che ti sei dato e puoi quindi memorizzare di più nella cache, ma devi ricordarti di considerare la validità del valore memorizzato nella cache di b. Se hai scelto 2 devi comunque controllare bogni volta ma puoi ricorrere alla cache per aifb verificare .

Se si stratificano le cache, è necessario considerare se si sono violate le "regole" del sistema come risultato del comportamento combinato.

Se sai che aha sempre validità se blo è, puoi organizzare la cache in questo modo (pseudocodice):

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

Ovviamente successivi stratificazione (diciamo x) è banale purché, in ogni fase della validità dell'ingresso appena aggiunto corrisponde a: brapporto di x: be x:a .

Tuttavia è del tutto possibile che tu possa ottenere tre input la cui validità era del tutto indipendente (o era ciclica), quindi non sarebbe possibile alcuna stratificazione. Ciò significherebbe che la riga contrassegnata // importante dovrebbe cambiare in

if (endCache [a] scaduto o non presente)


3
o forse, se il costo del controllo b è alto, usi pubsub in modo che quando b cambia notifica c. Il modello Observer è comune.
user1031420

15

Il problema nell'invalidazione della cache è che le cose cambiano senza che noi lo sappiamo. Quindi, in alcuni casi, una soluzione è possibile se c'è qualche altra cosa che ne sa e può avvisarci. Nell'esempio fornito, la funzione getData potrebbe agganciarsi al file system, che conosce tutte le modifiche ai file, indipendentemente dal processo che modifica il file, e questo componente a sua volta potrebbe notificare il componente che trasforma i dati.

Non credo che ci sia una soluzione magica generale per far sparire il problema. Ma in molti casi pratici ci possono essere ottime opportunità per trasformare un approccio basato sul "sondaggio" in uno basato sull'interruzione, che può semplicemente eliminare il problema.


3

Se hai intenzione di getData () ogni volta che esegui la trasformazione, hai eliminato l'intero vantaggio della cache.

Per il tuo esempio, sembra che una soluzione sarebbe per quando generi i dati trasformati, per memorizzare anche il nome del file e l'ora dell'ultima modifica del file da cui sono stati generati i dati (l'hai già memorizzato in qualsiasi struttura dati sia stata restituita da getData ( ), quindi copi semplicemente quel record nella struttura dati restituita da transformData ()) e poi quando chiami di nuovo transformData (), controlla l'ora dell'ultima modifica del file.


3

IMHO, Functional Reactive Programming (FRP) è in un certo senso un modo generale per risolvere l'invalidazione della cache.

Ecco perché: i dati obsoleti nella terminologia FRP sono chiamati glitch . Uno degli obiettivi di FRP è garantire l'assenza di difetti.

FRP è spiegato più dettagliatamente in questo discorso "Essence of FRP" e in questa risposta SO .

Nel discorso le Cells rappresentano un Oggetto / Entità memorizzato nella cache e un Cellviene aggiornato se una delle sue dipendenze viene aggiornata.

FRP nasconde il codice idraulico associato al grafico delle dipendenze e si assicura che non ci siano messaggi obsoleti Cell.


Un altro modo (diverso da FRP) a cui posso pensare è avvolgere il valore calcolato (di tipo b) in una sorta di Monade di scrittore in Writer (Set (uuid)) bcui Set (uuid)(notazione Haskell) contiene tutti gli identificatori dei valori mutabili da cui bdipende il valore calcolato . Quindi, uuidè una sorta di identificatore univoco che identifica il valore / variabile modificabile (ad esempio una riga in un database) da cui bdipende il calcolo .

Combina questa idea con combinatori che operano su questo tipo di writer Monad e ciò potrebbe portare a una sorta di soluzione generale di invalidazione della cache se usi questi combinatori solo per calcolare un nuovo b. Tali combinatori (diciamo una versione speciale di filter) prendono le monadi e (uuid, a)-s di Writer come input, dove aè un dato / variabile mutabile, identificato da uuid.

Quindi ogni volta che modifichi i dati "originali" (uuid, a)(ad esempio i dati normalizzati in un database da cui è bstato calcolato) da cui bdipende il valore calcolato di tipo , puoi invalidare la cache che contiene bse muti qualsiasi valore ada cui bdipende il valore calcolato , perché in base Set (uuid)a Writer Monad puoi dire quando questo accade.

Quindi ogni volta che muti qualcosa con un dato uuid, trasmetti questa mutazione a tutti i cache e questi invalidano i valori bche dipendono dal valore mutabile identificato con detto uuidperché la monade dello scrittore in cui bè avvolto può dire se ciò bdipende da detto uuido non.

Naturalmente, questo ripaga solo se leggi molto più spesso di quanto scrivi.


Un terzo approccio pratico consiste nell'utilizzare le visualizzazioni materializzate nei database e utilizzarle come cache. AFAIK mirano anche a risolvere il problema dell'invalidazione. Questo ovviamente limita le operazioni che collegano i dati mutabili ai dati derivati.


2

In questo momento sto lavorando a un approccio basato su PostSharp e funzioni di memorizzazione . L'ho passato davanti al mio mentore e lui concorda sul fatto che è una buona implementazione della memorizzazione nella cache in modo indipendente dal contenuto.

Ogni funzione può essere contrassegnata con un attributo che ne specifica il periodo di scadenza. Ogni funzione contrassegnata in questo modo viene memorizzata e il risultato viene memorizzato nella cache, con un hash della chiamata della funzione e dei parametri utilizzati come chiave. Sto usando Velocity per il backend, che gestisce la distribuzione dei dati della cache.


1

Esiste una soluzione o un metodo generale per creare una cache, per sapere quando una voce è obsoleta, in modo da avere la certezza di ottenere sempre dati aggiornati?

No, perché tutti i dati sono diversi. Alcuni dati potrebbero essere "obsoleti" dopo un minuto, altri dopo un'ora e alcuni potrebbero essere corretti per giorni o mesi.

Per quanto riguarda il tuo esempio specifico, la soluzione più semplice è avere una funzione di "controllo della cache" per i file, che chiami da entrambi getDatae transformData.


1

Non esiste una soluzione generale ma:

  • La cache può fungere da proxy (pull). Supponiamo che la tua cache conosca il timestamp dell'ultima modifica dell'origine, quando qualcuno chiama getData(), la cache chiede all'origine il timestamp dell'ultima modifica, se lo stesso, restituisce la cache, altrimenti aggiorna il suo contenuto con quello di origine e restituisce il suo contenuto. (Una variazione è il client che invia direttamente il timestamp sulla richiesta, la fonte restituirà il contenuto solo se il timestamp è diverso.)

  • È comunque possibile utilizzare un processo di notifica (push), la cache osserva la fonte, se la fonte cambia, invia una notifica alla cache che viene quindi contrassegnata come "sporca". Se qualcuno chiama getData()la cache verrà prima aggiornato al sorgente, rimuovere il flag "sporco"; quindi restituirne il contenuto.

La scelta in generale dipende da:

  • La frequenza: molte chiamate getData()preferirebbero un push in modo da evitare che la sorgente venga inondata da una funzione getTimestamp
  • Il tuo accesso alla fonte: possiedi il modello di origine? In caso contrario, è probabile che non sia possibile aggiungere alcun processo di notifica.

Nota: poiché l'utilizzo del timestamp è il modo tradizionale in cui funzionano i proxy http, un altro approccio consiste nella condivisione di un hash del contenuto archiviato. L'unico modo che conosco per 2 entità per aggiornarsi insieme è o ti chiamo (pull) o tu chiami me ... (push) tutto qui.



-2

Forse gli algoritmi che ignorano la cache sarebbero i più generali (o almeno, meno dipendenti dalla configurazione hardware), dal momento che useranno prima la cache più veloce e passeranno da lì. Ecco una conferenza del MIT su questo argomento : Cache Oblivious Algorithms


3
Penso che non stia parlando di cache hardware - sta parlando del suo codice getData () con una funzione che "memorizza nella cache" i dati che ha ottenuto da un file.
Alex319
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.