Invalidación de caché: ¿existe una solución general?

118

"Sólo hay dos problemas difíciles en la informática: invalidación de la caché y nombrar cosas".

Phil Karlton

¿Existe una solución o método general para invalidar un caché? saber cuándo una entrada está obsoleta, por lo que tiene la garantía de obtener siempre datos nuevos?

Por ejemplo, considere una función getData()que obtiene datos de un archivo. Lo almacena en caché en función de la última hora de modificación del archivo, que verifica cada vez que se llama.
Luego agrega una segunda función transformData()que transforma los datos y almacena en caché su resultado para la próxima vez que se llame a la función. No tiene conocimiento del archivo: ¿cómo agrega la dependencia de que si se cambia el archivo, esta caché deja de ser válida?

Puede llamar getData()cada vez que transformData()se llama y compararlo con el valor que se utilizó para construir la caché, pero eso podría terminar siendo muy costoso.

Greg
fuente
6
Creo que tiene algo que ver con escribir X Windows
Greg
1
Creo que ese título sería mejor como "Invalidación de caché: ¿existe una solución general?" ya que se refiere a una clase específica de problema de almacenamiento en caché.
RBarryYoung
71
No, no sabía mucho de informática. Estoy seguro de que su participación en la creación de OpenGL, X11 y SSLv3 lo puso demasiado ocupado para estudiarlo mucho. :-)
Tim Lesher
80
Solo hay 2 problemas difíciles en informática: invalidación de caché. Nombrar cosas. Y errores uno a uno.
The Dag
8
Una vez escuché esto como"The two hardest things in Computer Science are cache invalidation, naming things, and off-by-one errors."
Jonathon Reinhart

Respuestas:

55

De lo que estás hablando es del encadenamiento de dependencias de por vida, que una cosa depende de otra que puede modificarse fuera de su control.

Si tiene una función idempotente desde a, bhacia cdónde, si ay bson iguales, entonces ces lo mismo, pero el costo de verificación bes alto, entonces usted:

  1. acepta que en algún momento opera con información desactualizada y no siempre verifica b
  2. haz tu mejor esfuerzo para que la verificación sea lo bmás rápido posible

No puedes tener tu pastel y comértelo ...

Si puede colocar un caché adicional basado en ala parte superior, esto afectará el problema inicial ni un poco. Si elige 1, tiene la libertad que se dio a sí mismo y, por lo tanto, puede almacenar más en caché, pero debe recordar considerar la validez del valor en caché de b. Si elige 2, aún debe verificar bcada vez, pero puede recurrir a la caché para verificar asi se bretira.

Si coloca cachés en capas, debe considerar si ha violado las 'reglas' del sistema como resultado del comportamiento combinado.

Si sabe que asiempre tiene validez b, entonces puede organizar su caché así (pseudocódigo):

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

Obviamente capas sucesivas (por ejemplo x) es trivial, siempre que, en cada etapa de la validez de la entrada que acaba de agregar coincide con el a: brelación por x: by x: a.

Sin embargo, es muy posible que pueda obtener tres entradas cuya validez sea completamente independiente (o cíclica), por lo que no sería posible la superposición. Esto significaría que la línea marcada // importante tendría que cambiar a

si (endCache [a] expiró o no está presente)

ShuggyCoUk
fuente
3
o tal vez, si el costo de verificar b es alto, use pubsub para que cuando b cambie notifique a c. El patrón Observer es común.
user1031420
15

El problema de la invalidación de caché es que las cosas cambian sin que nos demos cuenta. Entonces, en algunos casos, una solución es posible si hay alguna otra cosa que sí sabe y puede notificarnos. En el ejemplo dado, la función getData podría conectarse al sistema de archivos, que sí conoce todos los cambios en los archivos, independientemente del proceso que cambie el archivo, y este componente a su vez podría notificar al componente que transforma los datos.

No creo que haya una solución mágica general para hacer que el problema desaparezca. Pero en muchos casos prácticos puede muy bien haber oportunidades para transformar un enfoque basado en "encuestas" en uno basado en "interrupciones", lo que puede hacer que el problema simplemente desaparezca.

El dag
fuente
3

Si va a getData () cada vez que realiza la transformación, entonces ha eliminado todo el beneficio del caché.

Para su ejemplo, parece que una solución sería que cuando genere los datos transformados, también almacene el nombre del archivo y la última hora de modificación del archivo desde el que se generaron los datos (ya almacenó esto en cualquier estructura de datos devuelta por getData ( ), por lo que simplemente copie ese registro en la estructura de datos devuelta por transformData ()) y luego, cuando llame a transformData () nuevamente, verifique la última hora de modificación del archivo.

Alex319
fuente
3

En mi humilde opinión, la programación reactiva funcional (FRP) es en cierto sentido una forma general de resolver la invalidación de caché.

He aquí el motivo: los datos obsoletos en la terminología de FRP se denominan fallos . Uno de los objetivos de FRP es garantizar la ausencia de fallos.

FRP se explica con más detalle en esta charla 'Esencia de FRP' y en esta respuesta SO .

En la charla, los Cells representan un objeto / entidad en caché y Cellse actualiza si se actualiza una de sus dependencias.

FRP oculta el código de plomería asociado con el gráfico de dependencia y se asegura de que no haya mensajes obsoletos Cell.


Otra forma (diferente de FRP) en la que puedo pensar es envolver el valor calculado (de tipo b) en algún tipo de Mónada de escritura Writer (Set (uuid)) bdonde Set (uuid)(notación Haskell) contiene todos los identificadores de los valores mutables de los que bdepende el valor calculado . Entonces, uuides una especie de identificador único que identifica el valor / variable mutable (digamos una fila en una base de datos) del cual bdepende el calculado .

Combine esta idea con combinadores que operan en este tipo de escritor Monad y que podrían conducir a algún tipo de solución general de invalidación de caché si solo usa estos combinadores para calcular un nuevo b. Tales combinadores (digamos una versión especial de filter) toman mónadas Writer y (uuid, a)-s como entradas, donde aes un dato / variable mutable, identificado por uuid.

Por lo tanto, cada vez que cambie los datos "originales" (uuid, a)(digamos, los datos normalizados en una base de datos a partir de la cual bse calculó) de los que bdepende el valor de tipo calculado , puede invalidar la caché que contiene bsi muta cualquier valor adel que bdepende el valor calculado , porque según el Set (uuid)de Writer Monad puedes saber cuándo sucede esto.

Entonces, cada vez que mutes algo con un dado uuid, transmites esta mutación a todos los cache-s y ellos invalidan los valores bque dependen del valor mutable identificado con dicho uuidporque la mónada Writer en la que bestá envuelto puede decir si eso bdepende de dicho uuido no.

Por supuesto, esto solo vale la pena si lee con mucha más frecuencia de la que escribe.


Un tercer enfoque práctico es usar vistas materializadas en bases de datos y usarlas como cachés. AFAIK también tienen como objetivo resolver el problema de invalidación. Por supuesto, esto limita las operaciones que conectan los datos mutables con los datos derivados.

jhegedus
fuente
2

Estoy trabajando en un enfoque en este momento basado en PostSharp y funciones de memorización . Lo pasé por alto a mi mentor, y él está de acuerdo en que es una buena implementación del almacenamiento en caché de una manera independiente del contenido.

Cada función se puede marcar con un atributo que especifica su período de caducidad. Cada función marcada de esta manera se memoriza y el resultado se almacena en la caché, con un hash de la llamada a la función y los parámetros utilizados como clave. Estoy usando Velocity para el backend, que maneja la distribución de los datos de la caché.

Chris McCall
fuente
1

¿Existe una solución o método general para crear una caché, para saber cuándo una entrada está obsoleta, de modo que tenga la garantía de obtener siempre datos nuevos?

No, porque todos los datos son diferentes. Algunos datos pueden estar "obsoletos" después de un minuto, algunos después de una hora y algunos pueden estar bien durante días o meses.

Con respecto a su ejemplo específico, la solución más simple es tener una función de 'verificación de caché' para los archivos, a la que llama desde getDatay transformData.

Cabra descontenta
fuente
1

No hay una solución general pero:

  • La caché puede actuar como un proxy (extracción). Suponga que su caché conoce la marca de tiempo del último cambio de origen, cuando alguien llama getData(), la caché le pide al origen la marca de tiempo del último cambio, si es la misma, devuelve la caché; de lo contrario, actualiza su contenido con la fuente y devuelve su contenido. (Una variación es que el cliente envíe directamente la marca de tiempo en la solicitud, la fuente solo devolvería contenido si su marca de tiempo es diferente).

  • Aún puede usar un proceso de notificación (push), la caché observa la fuente, si la fuente cambia, envía una notificación a la caché que luego se marca como "sucia". Si alguien llama getData()al caché, primero se actualizará a la fuente, elimine el indicador "sucio"; luego devuelva su contenido.

En general, la elección depende de:

  • La frecuencia: muchas llamadas getData()preferirían un empujón para evitar que la fuente se inunde con una función getTimestamp
  • Su acceso a la fuente: ¿posee el modelo fuente? De lo contrario, es probable que no pueda agregar ningún proceso de notificación.

Nota: Como el uso de la marca de tiempo es la forma tradicional en que funcionan los proxies http, otro enfoque es compartir un hash del contenido almacenado. La única forma que conozco para que 2 entidades se actualicen juntas es o te llamo (tira) o tú me llamas ... (empuja) eso es todo.

Flavien Volken
fuente
-2

Quizás los algoritmos ajenos a la caché serían los más generales (o al menos, menos dependientes de la configuración del hardware), ya que usarán primero la caché más rápida y seguirán adelante desde allí. Aquí hay una conferencia del MIT al respecto: caché de algoritmos ajenos

CookieOfFortune
fuente
3
Creo que no está hablando de cachés de hardware; está hablando de que su código getData () tiene una función que "almacena en caché" los datos que obtuvo de un archivo en la memoria.
Alex319