Una caché conserva una copia reutilizable de datos o resultados para evitar repetir trabajo más caro. Puede reducir consultas, cálculo y latencia, pero añade una decisión: cuánto tiempo puede utilizarse esa copia y qué ocurre cuando deja de estar disponible o actualizada.
El diseño empieza por el contrato de lectura. Un catálogo puede tolerar cierta demora en reflejar un cambio; una decisión sobre inventario o saldo necesita proteger su regla en el sistema autoritativo. El mismo dato puede tener usos con requisitos distintos.
Ubicación y coste del acceso
La caché puede estar dentro del proceso de la aplicación, en un servicio compartido como Redis, en el navegador o en un CDN. Estar cerca del consumidor reduce parte del recorrido, pero cercanía y velocidad deben medirse en la ruta real.
Una caché local evita un salto de red y tiene capacidad limitada por instancia. Cada proceso conserva su propia copia. Redis externo permite compartir entradas entre instancias, a cambio de conexiones, tráfico de red y una dependencia adicional. No toda caché reside exclusivamente en RAM: existen cachés que utilizan disco y otras capas de almacenamiento.
Una afirmación como 10,000 veces más rápido necesita una comparación concreta. La diferencia depende de la consulta evitada, la carga, el tamaño del resultado, la red y la serialización o deserialización. Una consulta indexada sencilla y un cálculo costoso no producen el mismo ahorro; un hit remoto también puede esperar por capacidad.
La clave debe representar el resultado que puede reutilizarse: entidad, parámetros y, cuando afectan la respuesta, tenant o permisos. El valor puede incluir una versión y datos sobre su frescura. Debe definirse qué lecturas admiten una copia y cuáles requieren consultar el origen.
Lectura con cache-aside
En cache-aside, la aplicación administra la caché junto al acceso a la base de datos. Para leer un dato:
- Busca la clave en la caché.
- Si encuentra una entrada utilizable, responde con ella: es un hit.
- Si no la encuentra, consulta la base de datos: es un miss.
- Intenta guardar una copia con el TTL correspondiente y responde con el dato obtenido.
--- config: look: handDrawn --- flowchart TD client["Cliente"] --> lookup["Aplicación<br/>Buscar clave"] lookup --> cache[("Redis compartido")] cache --> hit{"¿Hit utilizable?"} hit -->|"Sí"| response["Responder dato"] hit -->|"No: miss"| query["Aplicación<br/>Consultar origen"] query --> database[("Base de datos autoritativa")] database --> fill["Aplicación<br/>Intentar guardar copia con TTL"] fill --> cache fill --> response response --> client
La aplicación ejecuta la consulta y el llenado; Redis no consulta automáticamente la base de datos en este patrón. La figura representa una lectura con la caché disponible. Si falla guardar la copia después de leer el origen, todavía puede responderse con ese resultado. Una caída de Redis requiere además limitar el fallback para proteger la base.
El TTL define cuánto vive la entrada desde su inserción o actualización. Un plazo corto aumenta misses y trabajo de llenado; uno largo permite reutilizar más, pero exige resolver qué ocurre cuando cambia el dato original.
Arquitecturas de lectura y escritura
Los patrones de lectura deciden quién resuelve un miss. Los de escritura deciden cuándo se actualiza la base y qué significa aceptar una operación. Pueden combinarse: read-through puede acompañar write-through o write-behind. Estas capas requieren un adaptador; Redis estándar no integra automáticamente una base de datos externa.
| Patrón | Quién coordina | Cuándo termina la operación | Lectura | Coste o riesgo |
|---|---|---|---|---|
| Cache-aside | Aplicación. | Lectura: al obtener el dato. | Miss resuelto bajo demanda por la aplicación. | Consulta y llenado en cada miss. |
| Write-through | Capa de escritura. | Escritura: cuando BD y caché reportan éxito. | Según el patrón de lectura elegido. | Latencia sincrónica y posibles fallos parciales. |
| Write-behind / write-back | Capa con buffer de escrituras. | Escritura: al aceptar en la capa; BD después. | Debe considerar las escrituras pendientes. | Durabilidad del buffer y retraso de persistencia. |
| Read-through | Capa de lectura. | Lectura: al devolver el dato. | La capa consulta el origen y llena el miss. | Integración adicional; conserva costes y fallos de llenado. |
Cache-aside suele ser un punto de partida práctico cuando la aplicación ya controla el origen. Carga lo solicitado, pero no garantiza una caché pequeña: sin TTL, eviction y un presupuesto de capacidad también puede crecer o conservar datos poco útiles.
Write-through actualiza el origen y la caché sincrónicamente antes de comunicar éxito. Añade espera y puede cargar valores que nunca se leen. No aporta atomicidad ni consistencia fuerte por sí solo: una actualización puede terminar y la otra fallar. La concurrencia, las lecturas por caminos diferentes y las escrituras directas a la BD siguen requiriendo coordinación e invalidación según el contrato.
Write-behind acepta cambios antes de persistirlos en la BD. Puede reducir la espera de escritura, pero su buffer pendiente deja de ser una copia reconstruible desde esa base. Antes de confirmar una escritura cuya pérdida no se admite, debe establecerse su durabilidad, replicación y qué fallos cubre la confirmación. Aceptarla únicamente en memoria puede perderla si el proceso cae antes de persistirla.
La persistencia diferida necesita orden, reintentos idempotentes y protección frente a una escritura antigua que llegue tarde. También requiere observar el backlog y limitar admisión cuando no puede drenarse. Los cambios pendientes no deben expulsarse como copias descartables; llenar el buffer o perder contacto con la BD exige una política explícita.
Read-through traslada a la capa el acceso al origen y el llenado, simplificando ese camino de la aplicación. Define lecturas; la estrategia de escritura se elige aparte. Mover el código no elimina las carreras de frescura, los stampedes ni la necesidad de limitar el fallback.
Consistencia e invalidación
En cache-aside, una escritura puede confirmar el cambio en la base de datos y después invalidar la entrada correspondiente. Hacerlo después del commit evita tratar como definitivo un cambio que todavía podría fallar. La aplicación debe contemplar que la invalidación no llegue, se retrase o se repita.
La outbox transaccional puede registrar junto al cambio local un evento pendiente de invalidación. Su publicación y consumo necesitan recuperación e idempotencia. Ese mecanismo ayuda a conservar el trabajo pendiente; no elimina el intervalo durante el cual una caché puede servir la copia anterior.
También existe una carrera entre lectores y escritores:
- Un lector tiene un miss y obtiene del origen la versión 7.
- Un escritor confirma la versión 8 e invalida la caché.
- El lector anterior guarda la versión 7 con un TTL nuevo.
La caché queda repoblada con un dato antiguo aunque la invalidación haya ocurrido. El TTL no garantiza frescura fuerte ni, por sí solo, una edad máxima respecto al último cambio del origen. Limita la vida de la entrada que se guardó; esa entrada puede haber partido de una lectura antigua.
El diseño puede admitir esa ventana si el producto tolera datos obsoletos. Cuando necesita un contrato más exigente, puede versionar el dato y condicionar el llenado a una versión vigente. Esa coordinación debe funcionar también cuando la entrada está ausente: comparar únicamente contra un valor que acaba de borrarse no rechaza necesariamente un llenado antiguo. Una clave que incluye versión también necesita resolver cómo se obtiene la versión actual.
Los eventos de invalidación deben respetar ese contrato frente a retrasos y desorden. Si una decisión exige el estado actual, se consulta o valida en la autoridad que protege la regla; un hit no reemplaza esa validación. El caso de SD: Sistema de boletaje separa la vista rápida del inventario de la decisión transaccional sobre un asiento.
Capacidad y eviction
La memoria y el espacio de una caché son finitos. El presupuesto incluye valores, claves, metadatos y el margen que necesita el servicio. Guardar más entradas de las que caben obliga a expulsarlas o a rechazar nuevos llenados, según la política.
LRU, o Least Recently Used, prioriza expulsar datos que llevan más tiempo sin utilizarse. Redis ofrece políticas de LRU aproximadas, que seleccionan candidatos sin mantener un orden exacto de todas las entradas. La política elegida también determina si pueden expulsarse todas las claves o solo las que tienen expiración; una política sin eviction puede rechazar escrituras al alcanzar su límite.
Expiración y eviction cumplen funciones distintas. El TTL limita la vida de una entrada; la eviction recupera capacidad y puede eliminarla antes. Ambas producen misses. Elegir la política requiere observar la distribución del acceso y el coste de reconstruir los valores, no asumir que LRU sirve para cualquier carga.
En Kubernetes, los requests y límites de recursos llevan ese presupuesto a la operación del workload. Limitar la memoria del contenedor y elegir una política de eviction son decisiones relacionadas, con mecanismos distintos.
Una caché de copias debe poder reconstruirse desde su origen. Si Redis también conserva estado que no puede perderse, ese estado necesita su propio contrato de persistencia y recuperación; no debe tratarse automáticamente como material expulsable.
Stampede y coordinación del llenado
Un cache stampede ocurre cuando muchas peticiones necesitan reconstruir el mismo dato al mismo tiempo. Puede aparecer al vencer un TTL, después de una eviction, al reiniciar el servicio o con una caché fría. Un solo miss lógico termina generando muchas consultas al origen.
Singleflight, o agrupación de peticiones, permite que un proceso comparta el resultado de un llenado entre solicitudes concurrentes. Reduce trabajo dentro de esa instancia. Si hay varias instancias, cada una puede iniciar su propio llenado; una lease compartida puede reducir esa concurrencia entre procesos bajo su contrato de coordinación.
La lease tiene una duración acotada. Si vence mientras el primer worker sigue trabajando, otro puede empezar. Un fallo o cambio de primario también puede afectar la coordinación. La liberación debe comprobar la identidad del propietario para no borrar una lease ajena. Ese control no convierte un mutex en Redis en una exclusión durable frente a cualquier fallo, ni impide por sí solo publicar un resultado antiguo.
El jitter en los TTL reparte vencimientos de distintas claves y reduce expiraciones sincronizadas. No evita por sí solo la concurrencia sobre una clave caliente. Con stale-while-revalidate, pueden servirse copias anteriores mientras se renuevan, siempre que el producto admita esa obsolescencia y defina hasta cuándo utilizarlas.
Los llenados deben tener límites de concurrencia y espera. Si la coordinación falla, la política decide entre servir una copia aceptable, esperar de forma acotada o rechazar trabajo. La protección del origen y la validación de versiones siguen siendo necesarias aunque se reduzcan los llenados duplicados.
Hot keys y distribución de carga
Una hot key concentra muchas lecturas o actualizaciones. El sharding distribuye claves entre nodos, pero una sola clave pertenece a un shard: añadir shards no divide automáticamente su carga.
Una caché local L1 delante de Redis puede absorber lecturas repetidas de esa clave. El beneficio depende de qué instancias reciben tráfico y de cuánto puede reutilizarse la copia. Cada L1 necesita su TTL, límite de memoria y política de coherencia; repartir invalidaciones entre muchas instancias añade fanout y trabajo de recuperación.
La agrupación de solicitudes puede compartir lecturas o renovaciones concurrentes cuando el contrato lo permite. No debe retrasar indefinidamente las peticiones ni agrupar resultados que dependen de identidades diferentes.
Las réplicas pueden repartir ciertas lecturas, a cambio de aceptar el retraso de replicación permitido. No resuelven automáticamente la carga de escritura del primario y pueden devolver datos anteriores. La solución debe partir de la distribución real del tráfico, el tamaño del valor y la frescura requerida.
Fallos y verificación
Si Redis deja de responder, enviar todos los misses y timeouts directamente a la base de datos puede trasladar allí el pico que la caché estaba absorbiendo. El fallback necesita timeouts, límites de concurrencia y backpressure. Según el contrato, también puede servir datos anteriores aceptables o limitar admisión. La capacidad del origen debe comprobarse con la caché fría y durante una caída.
Con write-behind, leer la base como fallback puede omitir escrituras que la capa ya aceptó y todavía no persistió; esa lectura solo es válida si el contrato admite ese resultado.
La verificación debe hacer competir un llenado antiguo con una escritura e invalidación, duplicar eventos y retrasarlos. También debe vencer una lease durante una consulta, forzar expiraciones y evictions, y concentrar tráfico en una sola clave. El resultado esperado debe indicar qué datos pueden servirse y cuánto trabajo se admite en el origen.
Las señales útiles incluyen:
- Hit rate por tipo de consulta y ponderado por el coste o la carga evitada: muchos hits baratos pueden ocultar misses caros.
- Latencia p95 y p99 de la lectura completa, tanto en hits como en misses, incluyendo red y deserialización.
- Edad del dato y retraso de invalidación, usando una referencia del origen cuando se necesita medir frescura.
- Memoria, expiraciones, evictions y llenados rechazados.
- Carga y latencia de la base, llenados concurrentes y tráfico concentrado en hot keys.
Una caché queda justificada por el trabajo que evita y por el comportamiento que conserva cuando la copia envejece o desaparece. El ahorro debe medirse junto al contrato de lectura y la capacidad de recuperación.