En la mayoría de los diagramas de arquitectura, una caché es una caja entre un servicio y su base de datos. Esa caja esconde las preguntas que deciden si la caché ayuda o perjudica: quién mete los datos en ella, quién escribe en la base de datos y qué ve un lector entre medias.
Cuatro estrategias responden a esas preguntas de forma distinta: cache-aside, read-through, write-through y write-back. Esta guía recorre cada una con el mismo ejemplo, las compara, repasa los fallos que aparecen en producción y termina con un diagrama que puedes reutilizar.
Dos preguntas que responde toda caché
Cada estrategia de caché es un par de respuestas:
- ¿Quién carga los datos en la caché al leer? O tu código de aplicación o una capa de caché que sabe llegar a la base de datos.
- ¿Quién escribe en la base de datos, y cuándo? Tu aplicación, la capa de caché en la misma petición o la capa de caché más tarde.
| Estrategia | Lectura: quién llena la caché | Escritura: quién escribe en la base de datos |
|---|---|---|
| Cache-aside | La aplicación, tras un fallo de caché | La aplicación, que después invalida la clave |
| Read-through | La capa de caché, tras un fallo de caché | No lo define la estrategia; suele combinarse con una de las dos siguientes |
| Write-through | Suele combinarse con read-through o cache-aside | La capa de caché, en la misma petición |
| Write-back (write-behind) | Suele combinarse con read-through | La capa de caché, más tarde y a menudo por lotes |
El ejemplo que se usa en toda la guía es una API de productos con una tabla products en Postgres y una caché Redis. Una página de producto lee product:42 y una herramienta de administración actualiza los precios.
Cache-aside (carga diferida)
La aplicación habla con los dos almacenes. Primero pregunta a la caché; si falla, lee la base de datos y guarda el resultado en la caché con un tiempo de vida (TTL). La documentación de ElastiCache de AWS describe el mismo patrón como lazy loading.
def get_product(product_id):
key = f"product:{product_id}"
cached = redis.get(key)
if cached is not None:
return json.loads(cached) # hit
row = db.fetch_one("SELECT * FROM products WHERE id = %s", product_id)
if row is not None:
redis.set(key, json.dumps(row), ex=300) # fill, with a 300 s TTL
return row
def update_price(product_id, price):
db.execute("UPDATE products SET price = %s WHERE id = %s", price, product_id)
redis.delete(f"product:{product_id}") # invalidate, don't overwrite
Al escribir, borra la clave en lugar de escribir el valor nuevo en la caché. La siguiente lectura carga la fila actual. El patrón cache-aside de Microsoft describe la misma regla de escritura.
Lo que ganas: solo se cachean los datos que alguien ha pedido y, si la caché cae, las lecturas siguen funcionando contra la base de datos, solo que más despacio. Lo que pagas: cada fallo cuesta tres viajes de ida y vuelta (caché, base de datos, caché) y hay una condición de carrera. Un lector que falló puede leer la fila antigua, el escritor actualiza la fila y borra la clave, y luego el lector vuelve a escribir el valor antiguo. Queda obsoleto hasta que vence el TTL, una de las razones por las que cache-aside siempre necesita TTL.
Read-through
La aplicación solo llama a la caché. Si hay un fallo, la capa de caché carga el valor desde la base de datos mediante un cargador que configuras, lo guarda y lo devuelve. El resultado de la lectura es el mismo que con cache-aside, pero el código de carga vive en un solo sitio en lugar de en cada llamador.
Un servidor Redis normal no lee de tu base de datos por sí mismo. El read-through lo aporta una capa que sí sabe hacerlo: una biblioteca de caché con una función de carga, un proxy o una caché gestionada pensada para una base de datos concreta. Amazon DynamoDB Accelerator (DAX) es un ejemplo: ante un fallo de caché, pasa la lectura a DynamoDB, devuelve el resultado y lo cachea.
La contrapartida: la capa de caché pasa a estar en el camino de lectura. Si falla, las lecturas fallan, salvo que el cliente tenga un plan B contra la base de datos.
Write-through
La aplicación escribe en la capa de caché y la capa escribe en la base de datos en la misma operación. La aplicación solo recibe un éxito cuando ambas escrituras salen bien. DAX documenta exactamente esto: los datos se escriben primero en la tabla de DynamoDB, después en la caché, y la llamada solo tiene éxito si las dos escrituras lo tienen.
Lo que ganas: los datos escritos a través de la caché están al día en ella, así que una lectura justo después de una escritura los ve. Lo que pagas: cada escritura espera a dos almacenes, y la caché se llena de datos que se escriben pero nunca se leen. Añade un TTL para que caduquen. Además, un nodo de caché nuevo y vacío no tiene nada hasta que los datos se vuelven a escribir; por eso write-through suele combinarse con read-through o con carga diferida.
Write-back (write-behind)
La aplicación escribe en la caché y recibe un éxito de inmediato. La capa de caché escribe en la base de datos más tarde, a menudo por lotes, y las actualizaciones repetidas de una misma clave se convierten en una sola escritura en la base.
Lo que ganas: la menor latencia de escritura y muchas menos escrituras en la base durante los picos. Lo que pagas: todo lo que aún no se ha volcado se pierde si cae el nodo de caché, salvo que el búfer esté replicado o respaldado por un log duradero. La base de datos, además, va por detrás de la caché, así que los informes, otros servicios y las copias de seguridad ven datos más antiguos. Write-back encaja con contadores de visitas y datos parecidos, donde perder unos segundos es aceptable.
Lado a lado
| Cache-aside | Read-through | Write-through | Write-back | |
|---|---|---|---|---|
| El código de la app conoce la base de datos | Sí | No | No | No |
| Latencia de escritura | Escritura en la base más un borrado en la caché | Depende de la estrategia de escritura | Caché y base de datos | Solo caché |
| Lecturas obsoletas | Hasta la invalidación o el TTL | Hasta la invalidación o el TTL | Raras en escrituras que pasan por la caché | La base va por detrás de la caché |
| Si se pierde un nodo de caché | Lecturas más lentas | Las lecturas fallan sin plan B | Faltan datos hasta que se reescriben | Se pierden las escrituras no volcadas |
| Uso típico | La opción por defecto con Redis | Una biblioteca o una caché gestionada | Frescura justo después de escribir | Contadores con muchas escrituras y picos |
TTL, desalojo y tres fallos clásicos
Un TTL limita lo obsoleto que puede llegar a estar un valor. Elígelo por tipo de dato según la obsolescencia que acepte el producto: la descripción de un producto puede vivir minutos; el nivel de stock, quizá segundos. Cuando se lee una clave caducada, se comporta como si no existiera.
El desalojo (eviction) es un mecanismo distinto: lo que la caché quita cuando se queda sin memoria. En Redis, maxmemory fija el límite y maxmemory-policy elige qué se quita. El valor por defecto en redis.conf es noeviction, que devuelve errores a los comandos que añaden datos cuando la memoria está llena. Para una caché pura, la documentación de Redis sugiere allkeys-lru como buen valor por defecto. Los servicios gestionados fijan sus propios valores, así que revisa el tuyo. Mide la tasa de aciertos a partir de INFO stats como keyspace_hits / (keyspace_hits + keyspace_misses).
Hay tres fallos que aparecen en casi todas las cachés:
- Estampida. Una clave muy usada caduca y cientos de peticiones fallan a la vez y consultan todas la base de datos. Deja que una sola petición reconstruya el valor y que las demás esperen un poco o sirvan el valor antiguo. Un bloqueo en Redis lo consigue:
SET lock:product:42 <token> NX PX 5000solo tiene éxito para un llamador. El artículo de Facebook sobre memcache resuelve el mismo problema con leases e informa de que, en las claves propensas, el pico de consultas a la base bajó de 17 000 a 1300 por segundo. - Penetración. Las peticiones de IDs que no existen fallan siempre y llegan siempre a la base de datos. Cachea el resultado "no encontrado" con un TTL corto, o consulta antes un filtro de Bloom con los IDs válidos.
- Avalancha. Muchas claves caducan a la vez, por ejemplo tras una carga masiva o un reinicio de la caché, y la base de datos recibe toda la carga de golpe. Añade variación aleatoria a los TTL, como 300 segundos más hasta 60, y calienta la caché de forma gradual.
El diagrama
Cuatro filas con los mismos tres participantes, así que la diferencia está en las conexiones: quién habla con la base de datos, y cuándo.
Contenido del diagrama
- Product API
- Redis
- Postgres
- Cache layer
- 1 GET (miss)
- 2 SELECT
- 3 SET, TTL 300 s
- 1 read
- 2 on miss: SELECT
- 1 write
- 2 UPDATE, same request
- 1 write, OK at once
- 2 flush later, batched
Cómo leerlo:
- Fila 1, cache-aside. La API de productos lee Redis (1), lee Postgres por su cuenta si falla (2) y llena Redis con un TTL (3). La conexión a Postgres se salta la caché: la aplicación es dueña de las dos llamadas.
- Fila 2, read-through. La API solo habla con la capa de caché, que carga desde Postgres si hay un fallo.
- Fila 3, write-through. La escritura pasa por la capa de caché hasta Postgres en la misma petición.
- Fila 4, write-back. La API recibe su éxito al momento. La conexión discontinua es el volcado posterior, por lotes.
Cómo dibujar cachés en diagramas de arquitectura
- Nombra la estrategia y el TTL. "Cache-aside, TTL 300 s" en el nodo de caché o en su conexión responde a la primera pregunta de quien revisa.
- Dibuja quién habla con la base de datos. En cache-aside, el servicio tiene una conexión con la base de datos además de la que tiene con la caché. Una caché dibujada en línea delante de la base sugiere read-through, y los lectores lo darán por hecho.
- Dibuja el camino de invalidación. Muestra quién borra claves, incluidos los procesos por lotes y las herramientas de administración que escriben directamente en la base. Un escritor sin camino hacia la caché es la fuente de los datos obsoletos.
- Escribe el comportamiento ante fallos. "Caché caída: las lecturas van a Postgres" le dice más a la guardia que un segundo icono de caché.
- Pon los números en el lienzo. El objetivo de tasa de aciertos, el límite de memoria y la política de desalojo caben en una nota pequeña junto a la caché.
- Pon en discontinuo todo lo asíncrono. Un volcado de write-back o un evento de invalidación no forman parte de la petición.
Cómo elegir
Empieza con cache-aside y un TTL. Funciona con cualquier caché y cualquier base de datos, y si la caché falla, solo empeora la velocidad de las lecturas. Pasa a read-through cuando una biblioteca o una caché gestionada te dé un cargador. Añade write-through cuando las lecturas justo después de escribir deban ver el valor nuevo y las escrituras puedan permitirse la latencia extra. Usa write-back solo si puedes perder o reproducir las escrituras que no se han volcado.
Para ver el contexto alrededor de la caché, consulta el ejemplo de diagrama de diseño de sistemas, donde Redis sirve el camino de lectura de un acortador de URL, y las reglas del diagrama de arquitectura de microservicios para mostrar qué servicio es dueño de qué datos.
Constrúyelo en Archboard
- Inicia sesión con Google o GitHub y crea un diagrama.
- Para cada estrategia, añade un título Texto del pack Básico y coloca tres nodos en fila: un Contenedor del pack Modelo C4 para la API de productos, un Redis del pack Datos para la caché y un Postgres del pack Datos para la base de datos. En las filas 2 a 4, etiqueta la caché como "Cache layer".
- Conecta cada fila con etiquetas numeradas: "1 GET (miss)", "2 SELECT" y "3 SET, TTL 300 s" para cache-aside, y los pasos de lectura o escritura en las demás filas. Haz que la conexión de cache-aside entre la API y Postgres pase por encima de la caché.
- Pon en discontinuo la conexión de volcado de write-back.
- Añade bajo cada fila una nota Texto del pack Básico con la regla de escritura y lo que pasa cuando la caché falla.
El ejemplo usa 12 nodos de diagrama, dentro del plan Free. Para el método general detrás de cualquier diagrama de arquitectura, consulta cómo dibujar un diagrama de arquitectura de software.
Preguntas frecuentes
¿Cache-aside es lo mismo que lazy loading? Sí. Los dos nombres describen una aplicación que carga datos en la caché solo después de un fallo. La documentación de AWS habla de "lazy loading"; muchas otras fuentes dicen "cache-aside".
¿Actualizo o borro la clave de caché cuando cambian los datos? En cache-aside, bórrala. Dos escritores que guardan cada uno su valor nuevo en la caché pueden terminar en el orden equivocado y dejar un valor antiguo. Un borrado seguido de una carga nueva lo evita, y el TTL acota la condición de carrera más rara que se describe arriba.
¿Puede Redis hacer read-through o write-through por sí solo? No. Un servidor Redis guarda lo que le envían los clientes. Read-through y write-through necesitan una capa que conozca tu base de datos: una biblioteca de caché, un proxy o una caché gestionada para una base de datos concreta.
¿Qué TTL debería usar? La mayor obsolescencia que el producto acepte para ese tipo de dato. Después añade variación aleatoria para que las claves que se cargaron juntas no caduquen juntas.
¿Es seguro write-back? Solo si perder o reproducir las escrituras no volcadas es aceptable, o si el búfer de escritura está replicado o respaldado por un log duradero. Evítalo para datos como pagos o pedidos.