Be Software Architecture Wise
Be Wise Apps

Read in English · Lire en français · Auf Deutsch lesen

Sección 7.6 de 7.8 · Intermedio · 24 min de lectura

Estrategias de caché y sus trampas

Videolección

5 min · narrado en inglés, con subtítulos
Solo escuchar (audio)
Leer la transcripción

Las cachés hacen que los sistemas sean rápidos, y también pueden tumbarlos. En esta lección aprenderás cómo funciona una caché, cómo mantener sus copias lo bastante frescas y cómo evitar que falle en el peor momento.

Imagina una cafetería con mucho movimiento. En lugar de moler granos para cada pedido, el barista tiene sobre el mostrador un pequeño tarro de café molido. La mayoría de los pedidos se sirven desde el tarro en segundos. Solo cuando se vacía va alguien al almacén. El tarro es una caché. Hace que el servicio sea rápido, y le ahorra trabajo al almacén. Pero plantea preguntas. ¿Cuánto tiempo puede reposar el café molido antes de perder el sabor? ¿Y qué pasa cuando el tarro se vacía en plena hora punta de la mañana? Las cachés de software plantean exactamente las mismas preguntas.

Una caché guarda una copia de los datos en algún lugar más rápido que su ubicación original. Reduce la latencia y protege la base de datos, porque cada acierto es una consulta que la base de datos nunca ve. El precio se paga en corrección. Una caché es una segunda copia de los datos, y dos copias pueden no coincidir. Así que la primera pregunta no es qué caché usar. Es qué puede estar desactualizado, cuánto y para quién. Una carta con cinco minutos de antigüedad está bien. Un saldo bancario con cinco minutos de antigüedad quizá no.

El patrón seguro por defecto se llama cache-aside. Aquí lo usa un servicio de carta. El servicio pregunta primero a la caché. Si hay un acierto, el valor vuelve directamente. Si hay un fallo, lee la base de datos, el sistema de registro, y almacena el resultado en la caché con un tiempo de vida, tras el cual la entrada caduca. Las escrituras siguen otro camino. Actualiza primero la base de datos y luego borra la entrada de la caché. No escribas el nuevo valor en la caché. La siguiente lectura simplemente falla y recarga el valor actualizado. Y si la caché desaparece por completo, el servicio simplemente lee la base de datos, más despacio. Por eso cache-aside falla de forma segura.

Mantener fresca una caché es notoriamente difícil. Una frase ingeniosa atribuida a Phil Karlton dice que solo hay dos cosas difíciles en informática: la invalidación de caché y ponerles nombre a las cosas. Veamos por qué. Incluso borrar al escribir deja una pequeña condición de carrera. Un lector no encuentra el dato en la caché y lee de la base de datos un precio de nueve libras. Luego se pausa, quizá por una red lenta. Mientras tanto, un escritor actualiza el precio a once libras y borra la entrada de la caché. Hasta aquí, todo bien. Entonces el lector se reanuda y guarda nueve libras en la caché. El precio obsoleto se queda hasta que se agota su tiempo de vida. Por eso los buenos diseños combinan ambas cosas: invalidar al producirse un cambio y mantener un tiempo de vida como red de seguridad.

La proporción de búsquedas que responde la caché es su tasa de aciertos. Supongamos que un servicio de carta recibe veinte mil lecturas por segundo. La caché responde en medio milisegundo, la base de datos en ocho, y la base de datos aguanta unas tres mil consultas por segundo. Sin caché, cada lectura va a la base de datos: casi siete veces lo que puede aguantar. Con una tasa de aciertos del ochenta por ciento, siguen llegando a la base de datos cuatro mil consultas por segundo. Eso supera la capacidad, así que se forman colas y la latencia sube. Con el noventa y cinco por ciento, son mil consultas por segundo. Holgado. Con el noventa y nueve por ciento, la latencia media apenas cambia, pero la carga de la base de datos se divide por cinco. Cada nueve adicional de aciertos divide la carga de la base de datos por unas diez. Ahora la caché se reinicia vacía. Las veinte mil consultas por segundo llegan a una base de datos dimensionada para tres mil, y esta se cae. Una caché sin la que no puedes funcionar es una dependencia crítica.

Una estampida se produce cuando una entrada popular caduca y cientos de solicitudes fallan en el mismo instante y van todas a la base de datos. Las defensas se combinan bien. La agrupación de solicitudes permite que solo una solicitud por clave vaya a buscar el valor, mientras las demás esperan su resultado. La caducidad con jitter añade una cantidad aleatoria a cada tiempo de vida, para que las entradas escritas a la vez no caduquen a la vez. Stale-while-revalidate sirve el valor caducado a todos mientras una solicitud en segundo plano obtiene uno nuevo. Y precalentar una caché nueva a partir de una lista de claves muy solicitadas, antes de que reciba tráfico, evita la estampida tras un reinicio.

Por último, cuatro errores comunes. Añadir una caché antes de medir: a menudo la solución real es un índice o una consulta mejor. No tener ningún tiempo de vida, de modo que una ruta de código olvidada deja datos obsoletos para siempre. Dimensionar la base de datos solo para la carga con caché, de modo que un reinicio de la caché tumba toda la plataforma. Y almacenar datos personales o financieros en cachés compartidas. Una página con el nombre de un cliente, guardada en caché solo bajo su URL, puede servirse al siguiente visitante.

La sección completa compara cinco patrones de caché y trata los niveles de caché, las cabeceras HTTP y el desalojo. A continuación, en la Sección siete punto siete, veremos la evolución de esquemas. Nos vemos allí.

Lo que necesitas saber

Por qué usar una caché y qué cuesta

Una caché guarda una copia de los datos en algún lugar más rápido o más cercano que su ubicación original, para que las solicitudes repetidas no paguen el precio completo cada vez. La Sección 0.2 presentó la idea como un intercambio de espacio por tiempo. A escala de arquitectura, el intercambio es más interesante, porque una caché cambia tres cualidades a la vez. Reduce la latencia (una búsqueda en Redis en el mismo centro de datos suele tardar mucho menos de un milisegundo; una consulta a la base de datos que une cinco tablas puede tardar decenas de milisegundos). Aumenta el rendimiento (throughput) y protege la base de datos, porque cada acierto (hit) es una consulta que la base de datos nunca ve. Y puede mejorar la disponibilidad, porque una caché capaz de servir datos algo antiguos sigue funcionando mientras la fuente está lenta o caída.

El precio se paga en corrección y en complejidad. Una caché es una segunda copia de los datos, y dos copias pueden no coincidir. Por tanto, toda caché encarna una decisión sobre cuán desactualizada puede estar una respuesta, y para quién. Una carta de restaurante con cinco minutos de antigüedad es aceptable; un saldo bancario con cinco minutos de antigüedad, mostrado a un cliente que decide si puede permitirse un pago, no lo es. Las cachés también añaden un componente que puede fallar, consumen memoria que cuesta dinero y crean nuevos modos de fallo, como las estampidas y los arranques en frío, que no existen sin ellas. Por eso la primera pregunta no es ¿qué caché?, sino ¿qué puede estar desactualizado, cuánto y qué pasa si lo está?

La eficacia de una caché se mide por su tasa de aciertos: los aciertos divididos entre todas las búsquedas. La tasa de aciertos depende de lo sesgado que sea el patrón de acceso. El tráfico de lectura de la mayoría de los sistemas de consumo está muy sesgado (una pequeña fracción de los restaurantes, productos o cuentas recibe la mayoría de las lecturas), así que una caché que contiene un pequeño porcentaje de los datos a menudo puede responder a la mayoría de las lecturas. El acceso uniformemente aleatorio, en cambio, se aprovecha mal de la caché hagas lo que hagas.

Los cinco patrones clásicos

Los patrones de caché se diferencian en quién carga la caché, en cuándo llegan las escrituras a la fuente y, por tanto, en cuán desactualizados y cuán duraderos pueden ser los datos. La tabla los compara; los párrafos que la siguen explican dónde encaja cada uno.

Comparación de los patrones de caché

PatrónCómo funcionan las lecturasCómo funcionan las escriturasVentajasRiesgos
Cache-aside (carga diferida)La aplicación consulta la caché; en caso de fallo (miss), lee la base de datos y almacena el resultadoLa aplicación escribe en la base de datos y luego borra (o actualiza) la entrada de la cachéSencillo; solo almacena en caché lo que se usa; si la caché falla, todo se reduce a lecturas más lentasLa primera lectura tras la caducidad es lenta; las condiciones de carrera pueden dejar entradas desactualizadas; estampidas
Read-throughLa aplicación pide los datos a la caché; la propia biblioteca de caché los carga desde la base de datos en caso de falloSuele combinarse con escrituras write-through o cache-asideLa lógica de carga está en un solo lugarLos mismos problemas de desactualización y estampidas; la capa de caché debe conocer la fuente de datos
Write-throughComo en read-through o cache-asideLa escritura va a la caché y a la base de datos de forma síncrona antes de confirmarseLa caché nunca está más desactualizada que la base de datos para los datos escritos a través de ellaCada escritura paga ambas latencias; almacena en caché datos que quizá nunca se lean
Write-behind (write-back)Se sirven desde la cachéLa escritura se confirma en cuanto está en la caché; se vuelca a la base de datos más tarde, a menudo por lotesEscrituras muy rápidas; absorbe ráfagas; agrupa en lotes el trabajo de la base de datosPérdida de datos si la caché falla antes del volcado; la base de datos va con retraso; problemas de orden y conflictos
Refresh-aheadSe sirven desde la cachéNo afecta a las escrituras: las entradas muy solicitadas se recargan poco antes de caducarLos elementos populares nunca caducan en las narices de los usuariosRecargas desperdiciadas de elementos que no se habrían leído; necesita estadísticas de acceso

Cache-aside es la opción por defecto por una buena razón: es fácil razonar sobre él y falla de forma segura, porque si la caché desaparece la aplicación simplemente lee la base de datos (más despacio). Read-through traslada la misma lógica a una biblioteca o un producto de caché, lo que resulta ordenado, pero no cambia el comportamiento. Write-through conviene para los datos que se leen poco después de escribirse, como la sesión de un usuario o su carrito de la compra. Write-behind es el arriesgado: convierte la caché en el sistema de registro temporal, así que solo es apropiado cuando perder los últimos segundos de escrituras es aceptable (contadores de visitas, eventos de analítica, puntuaciones de juegos) o cuando la propia caché está replicada y es duradera. Nunca uses write-behind para el dinero. Refresh-ahead es una optimización para un pequeño conjunto de claves muy solicitadas, como la lista de restaurantes de la página de inicio en una ciudad con mucha actividad.

Cache-aside en la práctica

Cache-aside: al leer, el servicio consulta la caché, carga desde la base de datos si hay un fallo y almacena el resultado; al escribir, actualiza la base de datos y luego borra la entrada en cachéServicio Cartacódigo de la appCachép. ej. RedisBDsist. de registro1 GET clave2 hit: valor5 SET clave, TTL3 miss: SELECT4 filaESCRITURAa UPDATE filaen base de datosb DELETE claveen la cachépróxima lecturafalla y cargavalor nuevo
Las lecturas llenan la caché de forma diferida cuando hay un fallo; las escrituras cambian primero la base de datos y después borran la copia en caché en lugar de sobrescribirla.
import json, random

TTL = 300  # seconds

def get_menu(restaurant_id):
    key = f"menu:{restaurant_id}:v3"            # version the key with the data shape
    cached = cache.get(key)
    if cached is not None:
        return json.loads(cached)                # hit
    with cache.lock(f"lock:{key}", timeout=5):  # coalesce: one loader per key
        cached = cache.get(key)                  # someone may have filled it meanwhile
        if cached is not None:
            return json.loads(cached)
        menu = db.load_menu(restaurant_id)       # miss: go to the system of record
        jitter = random.randint(0, TTL // 10)    # spread expiry times out
        cache.set(key, json.dumps(menu), ex=TTL + jitter)
        return menu

def update_price(restaurant_id, item_id, price):
    db.update_price(item_id, price)              # 1. change the system of record
    cache.delete(f"menu:{restaurant_id}:v3")     # 2. invalidate, do not overwrite
Cache-aside con una clave versionada, un bloqueo por clave para agrupar los fallos concurrentes, una caducidad con jitter y la invalidación por borrado después de la escritura en la base de datos.

Tiempo de vida e invalidación

Hay dos formas de impedir que una caché sirva datos que han cambiado. La primera es un tiempo de vida (TTL): cada entrada caduca tras un periodo fijo, lo que acota la desactualización sin ninguna coordinación. Un TTL de cinco minutos en las cartas significa que un cambio de precio es visible en todas partes en un plazo de cinco minutos. La segunda es la invalidación explícita: cuando la fuente cambia, algo borra o actualiza la copia en caché. La invalidación da datos más frescos, pero exige que cada ruta del código que modifica los datos conozca todas las cachés que los contienen, que es justo lo que falla a medida que los sistemas crecen. En los sistemas distribuidos, la invalidación suele estar dirigida por eventos: el servicio de carta publica MenuChanged y cada servicio que tiene una copia borra su entrada. La mayoría de los diseños profesionales combinan ambas: invalidan al producirse un cambio para tener datos frescos y mantienen un TTL como red de seguridad para la invalidación que se haya escapado.

La dificultad queda recogida en una frase ingeniosa atribuida a Phil Karlton, de Netscape: Solo hay dos cosas difíciles en informática: la invalidación de caché y ponerles nombre a las cosas. Es difícil porque la invalidación es un problema de sistemas distribuidos disfrazado. La escritura en la base de datos y el borrado en la caché son dos operaciones en dos sistemas sin una transacción compartida, así que una puede tener éxito mientras la otra falla, y las operaciones de distintas solicitudes pueden intercalarse.

Dos reglas evitan la mayoría de los problemas. Primera: en una escritura, actualiza la base de datos y luego borra la entrada de la caché; no escribas el nuevo valor en la caché. Dos escritores concurrentes que actualizan cada uno la caché pueden terminar en el orden inverso al de sus commits en la base de datos y dejar el valor más antiguo en caché indefinidamente, mientras que un borrado solo puede causar un fallo adicional. Segunda: acepta que incluso borrar al escribir deja una pequeña condición de carrera, que se muestra a continuación, y acótala con un TTL. El artículo de Facebook de 2013 sobre el escalado de memcache describe los leases (concesiones) para este caso: la caché da un token al cliente que tuvo el fallo y rechaza la escritura posterior de ese cliente si la clave se borró entretanto.

La carrera del SET obsoleto

Carrera en cache-aside: un lector lento guarda un valor antiguo después de que un escritor borre la entrada en caché; el dato obsoleto dura hasta que vence el TTLLector REscritor WEn cachéGET menu:17 falla(nada)SELECT: precio £9UPDATE precio a £11DELETE menu:17(nada)SET menu:17 = £9£9 obsoleto hasta TTLR se pausa(GC, red lenta)
Borrar al escribir reduce la ventana de datos obsoletos, pero no la cierra; un TTL acota cuánto tiempo sobrevive el valor obsoleto.

Estampidas y el thundering herd

Una cache stampede (estampida de caché) se produce cuando una entrada popular caduca, o un nodo de caché se reinicia vacío, y cientos de solicitudes concurrentes fallan en el mismo instante. Cada una va a la base de datos a calcular el mismo valor, y la base de datos, dimensionada para la tasa de fallos normal de la caché, se ve desbordada. Las respuestas más lentas hacen que se acumulen más solicitudes, y la sobrecarga puede propagarse en cascada. El fenómeno relacionado del thundering herd (manada en estampida) describe a muchos clientes en espera que se despiertan a la vez para competir por un único recurso. Facebook informó de que, para un conjunto de claves especialmente propensas a esto, los leases redujeron la tasa máxima de consultas a la base de datos de 17 000 a 1300 por segundo.

Las defensas se combinan bien. La agrupación de solicitudes (request coalescing), también llamada single flight, permite que solo una solicitud por clave vaya a buscar el dato a la fuente mientras las demás esperan su resultado; dentro de un proceso es un mapa de futuros en curso, y entre procesos, un bloqueo breve en la caché, como en el código anterior. La caducidad con jitter añade una cantidad aleatoria a cada TTL para que las entradas escritas a la vez (tras un despliegue o un vaciado de la caché) no caduquen a la vez. Stale-while-revalidate sirve el valor caducado a todos mientras una solicitud en segundo plano obtiene uno nuevo, lo que elimina por completo el pico de latencia cuando una ligera desactualización es aceptable. La caducidad anticipada probabilística (Vattani, Chierichetti y Lowenstein, VLDB 2015) hace que cada lector decida al azar, con una probabilidad que aumenta a medida que se acerca la caducidad, si actualiza antes de tiempo, de modo que normalmente una solicitud actualiza la entrada antes de que caduque de verdad. Por último, el precalentamiento de una caché nueva a partir de una lista de claves muy solicitadas antes de que reciba tráfico evita la estampida del arranque en frío tras un reinicio.

Ejemplo práctico

Ejemplo práctico: lo que la tasa de aciertos hace con la latencia y la carga de la base de datos

El servicio de carta de la plataforma de reparto de comida recibe 20 000 lecturas de cartas por segundo en el pico de la noche. Una búsqueda en la caché tarda 0.5 ms; una consulta a la base de datos tarda 8 ms. En caso de fallo, el servicio paga ambas (8.5 ms) y almacena el resultado de forma asíncrona. La base de datos aguanta unas 3000 consultas de este tipo por segundo antes de que la latencia se dispare.

FórmulaLatencia media = 0.5 + (1 − h) × 8 ms. Carga de la base de datos = (1 − h) × 20 000 consultas por segundo.
Sin ninguna caché8 ms de media; 20 000 consultas por segundo, casi siete veces lo que la base de datos puede aguantar.
Tasa de aciertos del 80%0.5 + 0.2 × 8 = 2.1 ms; 4000 consultas por segundo: todavía por encima de la capacidad, así que la base de datos encola y la latencia sube.
Tasa de aciertos del 95%0.5 + 0.05 × 8 = 0.9 ms; 1000 consultas por segundo: holgado.
Tasa de aciertos del 99%0.5 + 0.01 × 8 = 0.58 ms; 200 consultas por segundo.
Del 95% al 99%La latencia media mejora solo 0.32 ms, pero la carga de la base de datos se divide por cinco, de 1000 a 200 consultas por segundo.
El clúster de caché se reinicia vacíoLa tasa de aciertos cae casi al 0% durante un tiempo: 20 000 consultas por segundo llegan a una base de datos dimensionada para 3000. Sin agrupación de solicitudes, precalentamiento ni limitación de tasa (rate limiting), la base de datos se cae.
PercentilesCon un 95% de aciertos, el 5% de las solicitudes tarda 8.5 ms, así que el p95 queda justo en el límite y el p99 es de 8.5 ms, no de 0.9 ms.

La tasa de aciertos importa mucho más para la base de datos que para la latencia media: cada nueve adicional de aciertos divide la carga de la base de datos por unas diez.

Una caché sin la que el sistema no puede funcionar ya no es una optimización; es una dependencia crítica, y hay que planificar su fallo como la Sección 2.5 planifica el de cualquier otra.

Las medias favorecen a las cachés; como mostró la Sección 2.4, los fallos definen la cola.

Niveles de caché

Hay cachés en todos los niveles entre el usuario y el disco, y una solicitud la responde el primer nivel que tiene una copia utilizable. El navegador mantiene una caché privada controlada por cabeceras HTTP. Una red de distribución de contenidos (CDN) mantiene copias compartidas en ubicaciones del borde (edge) cercanas a los usuarios, ideal para imágenes, scripts y respuestas de API públicas como las cartas. Un proxy inverso en el origen (Nginx, Varnish) puede almacenar en caché respuestas completas durante unos segundos, lo que se llama microcaching y puede absorber un pico repentino. La aplicación almacena en caché objetos calculados, ya sea en el propio proceso (lo más rápido, pero cada instancia tiene su propia copia y no coinciden entre sí) o en una caché compartida como Redis o Memcached. La base de datos almacena en caché páginas en memoria (los shared buffers de PostgreSQL, el buffer pool de InnoDB) y puede precalcular resultados costosos en vistas materializadas.

Cuanto más lejos de los datos, mayor es el ahorro por acierto y más difícil la invalidación. Puedes borrar una clave de Redis en un milisegundo; puedes purgar una CDN, más despacio; no puedes llegar en absoluto al navegador de un cliente, así que el max-age que envíes es una promesa con la que tendrás que vivir. Por eso las duraciones largas en el navegador se usan para archivos cuya URL cambia cuando cambia su contenido (app.3f9a1c.js), y las cortas, o la revalidación, para todo lo demás.

Dónde viven las cachés

Cinco niveles de caché, del navegador a la memoria de la base de datos, pasando por una CDN, un proxy inverso y la caché de la aplicación, con los controles de cada unoNavegadorcaché privadaBorde CDNcompartida, cercanaProxy inversocompartida, origenAplicaciónen proceso o RedisBDbuffer pool, vistasUna solicitud avanza a la derecha solo lo necesario; cada nivel que responde ahorra el trabajo de todos los de detráscerca del usuario: menos latencia, más difícil invalidarcerca de los datos: más fácil refrescarCache-Control,ETags-maxage,API purgaTTL por ruta,microcachingdiseño clave,TTL, desalojogestionada porla propia BD
Cada nivel que responde a una solicitud ahorra trabajo a todos los niveles que hay detrás; los niveles más cercanos al usuario ahorran más, pero son más difíciles de invalidar.

Cabeceras de caché HTTP

HTTP tiene un modelo de caché completo, definido en el RFC 9111 (junio de 2022), y usarlo bien puede eliminar carga antes de que llegue a tus servidores. Cache-Control: max-age=60 permite que cualquier caché reutilice una respuesta durante 60 segundos; s-maxage lo sustituye para las cachés compartidas, como las CDN; private prohíbe que las cachés compartidas la almacenen (úsalo para todo lo personal); no-cache, de forma confusa, significa almacénala, pero consulta con el servidor antes de cada reutilización; y no-store significa no guardes nunca una copia, el ajuste correcto para los saldos y los extractos bancarios. Las extensiones stale-while-revalidate y stale-if-error (RFC 5861, 2010) aplican a nivel HTTP las ideas sobre estampidas y disponibilidad vistas antes.

Cuando una copia en caché está desactualizada, el cliente no necesita descargarla de nuevo. Un validador le permite preguntar si ha cambiado algo: el servidor envía con la respuesta un ETag (un identificador de versión opaco) o una fecha Last-Modified, y más tarde el cliente envía If-None-Match o If-Modified-Since. Si nada ha cambiado, el servidor responde 304 Not Modified sin cuerpo, lo que ahorra ancho de banda y, a menudo, la mayor parte del trabajo del servidor. Estas solicitudes condicionales se definen en el RFC 9110. La cabecera Vary indica a las cachés qué cabeceras de la solicitud (como Accept-Language) producen respuestas distintas, para que nunca se sirva una carta en francés a una solicitud en inglés.

GET /restaurants/17/menu HTTP/1.1
Host: api.example-food.test

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30
ETag: "menu-17-v42"

... 18 KB of menu JSON ...

# 100 seconds later the copy is past max-age plus the stale-while-revalidate
# window, so the browser must revalidate before using it:
GET /restaurants/17/menu HTTP/1.1
If-None-Match: "menu-17-v42"

HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30
ETag: "menu-17-v42"

# A bank balance, by contrast, must never sit in any cache:
Cache-Control: no-store
Una respuesta pública de carta que los navegadores pueden reutilizar durante 60 segundos y las CDN durante 300, revalidada de forma barata con un ETag; los datos financieros personales usan no-store.

Desalojo, dimensionamiento y riesgos de consistencia

Una caché es más pequeña que los datos que tiene detrás, así que, cuando se llena, debe desalojar algo. Menos recientemente usado (LRU) desaloja la entrada que lleva más tiempo sin tocarse y funciona bien cuando el uso reciente predice el uso futuro. Menos frecuentemente usado (LFU) desaloja la entrada que se usa con menos frecuencia y resiste los recorridos puntuales (un informe nocturno que toca una vez cada restaurante vaciaría una caché LRU de sus entradas realmente muy solicitadas). Las cachés de producción se aproximan a ambas: Redis, por ejemplo, toma una muestra de unas pocas claves y desaloja la mejor candidata entre ellas, y ofrece políticas como allkeys-lru, allkeys-lfu y volatile-ttl. Su política por defecto, noeviction, rechaza las escrituras cuando la memoria está llena, lo que conviene a Redis usado como almacén, pero no como caché, así que configura la política de forma deliberada.

Más allá de la desactualización, las cachés introducen riesgos de consistencia más sutiles. Un usuario que cambia su dirección de entrega y luego ve la antigua, porque la lectura vino de una caché que otra instancia aún no había invalidado, sufre la ruptura de la garantía de leer sus propias escrituras (Sección 7.4); una solución habitual es saltarse la caché para las escrituras recientes del propio usuario. Las cachés en proceso de muchas instancias no coinciden entre sí, así que solicitudes consecutivas enviadas a instancias distintas pueden mostrar un valor que retrocede. Almacenar en caché un error o un resultado vacío sin un TTL corto convierte una interrupción breve en una larga. Almacenar en caché datos que mezclan usuarios (una página con el nombre del cliente, guardada en caché solo bajo la URL) filtra datos personales al siguiente visitante, una clase de incidente de seguridad bien conocida. Y las claves de caché deben incluir todo lo que cambia la respuesta: el inquilino (tenant), el idioma, la moneda, los permisos y la versión del formato de los datos.

Comprueba lo que has entendido

¿Por qué una escritura debería borrar la entrada de la caché en lugar de poner el nuevo valor en la caché?

Porque dos escritores concurrentes pueden terminar sus actualizaciones de la caché en el orden inverso al de sus commits en la base de datos y dejar en caché el valor más antiguo; un borrado, en el peor caso, causa un fallo adicional que recarga el valor actual.

Una búsqueda en la caché cuesta 1 ms y una lectura de la base de datos 10 ms. ¿Cuál es, aproximadamente, la latencia media con una tasa de aciertos del 90% con cache-aside?

1 + 0.1 × 10 = 2 ms (cada solicitud paga la búsqueda; una de cada diez paga además la lectura de la base de datos).

¿Cuál es la diferencia entre Cache-Control: no-cache y no-store?

no-cache permite que una caché almacene la respuesta, pero le exige revalidarla con el servidor antes de cada reutilización; no-store prohíbe guardar ninguna copia.

¿Por qué puede importar subir la tasa de aciertos del 99% al 99.9% aunque la latencia media apenas cambie?

Porque la carga de la base de datos es proporcional a la tasa de fallos, que baja del 1% al 0.1%: las consultas que llegan a la base de datos se dividen por diez.

Siguiente · 7.7Evolución de esquemasCambiar los esquemas de bases de datos, API y eventos mientras el sistema sigue funcionando: compatibilidad hacia atrás, hacia adelante y completa, expandir y contraer, herramientas de migración, backfills de gran volumen y registros de esquemas para Avro y Protocol Buffers.Continuar →

Fuentes y lecturas recomendadas

¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.

Pregunta: “Nuestra API de búsqueda de restaurantes es lenta en las horas punta. ¿Cómo decido si añadir una caché, dónde ponerla y qué TTL usar?”