¿Cuáles son las principales estrategias de invalidación de caché (TTL, invalidación explícita, basada en eventos) y por qué la invalidación de caché es tan famosamente difícil?
La frase de Phil Karlton sobre "solo dos cosas difíciles en informática" es un cliché porque toda estrategia de invalidación cambia frescura por complejidad en algún punto.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cuáles son las principales estrategias de invalidación de caché (TTL, invalidación explícita, basada en eventos) y por qué la invalidación de caché es tan famosamente difícil?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Por qué "simplemente limpia la caché" nunca es toda la respuesta
Poblar una caché es el 20% fácil del problema. El 80% difícil es decidir cuándo un valor cacheado deja de ser cierto, que es por lo que la frase "solo hay dos cosas difíciles en informática: la invalidación de caché y ponerle nombre a las cosas" se repite en casi toda entrevista de backend. Es un chiste con un mecanismo real detrás: la corrección ahora depende de un hecho (¿esto cambió?) que vive completamente fuera de la caché, en el sistema de registro que produjo el valor.
Hay tres estrategias a las que recurren los equipos, normalmente combinadas y no de forma aislada.
#1. Expiración por TTL (time-to-live)
La estrategia más simple: cada entrada cacheada lleva un tiempo de expiración, después del cual se trata como inexistente sin importar si el dato subyacente cambió o no.
SET product:1001 '{"name":"Widget","price":19.99}' EX 60
o en código de aplicación:
redis.setex("product:1001", 60, json.dumps(producto))
El TTL es atractivo porque se autorrepara: nunca tienes que probar que encontraste todas las claves que podría afectar una escritura, porque de todos modos todo expira eventualmente. El costo es una ventana de desactualización garantizada: hasta por 60 segundos aquí, un cliente puede leer un precio que ya está equivocado. Si eliges un TTL demasiado corto, pierdes la mayor parte del beneficio de cachear (misses casi constantes); si lo eliges demasiado largo, un cambio de precio tarda minutos en llegar a los clientes. Los TTL también crean una estampida (thundering herd): si cacheas diez mil filas de productos con el mismo TTL de 60 segundos fijado al momento del despliegue, todas expiran juntas, y diez mil misses simultáneos golpean la base de datos a la vez. Agregar jitter al TTL (60 + random(0, 10) segundos) es la solución estándar.
#2. Invalidación explícita
En vez de esperar la expiración, el camino de escritura borra o actualiza activamente la(s) clave(s) de caché afectadas por un cambio.
def actualizar_precio(product_id, nuevo_precio):db.execute("UPDATE products SET price = %s WHERE id = %s", nuevo_precio, product_id)redis.delete(f"product:{product_id}")
Esto reduce la ventana de desactualización a, aproximadamente, el tiempo entre la escritura y la siguiente lectura. La trampa es que requiere saber, en el momento de escribir, todas las claves de caché que podrían derivarse de la fila modificada. La clave de caché de un solo producto es fácil. La clave de caché de "los 10 productos más vendidos esta semana", o una página de resultados de búsqueda que incluye ese producto, no se invalida de forma obvia con un cambio de precio en el producto 1001: no hay una sola clave que borrar, porque el valor cacheado es un agregado sobre datos cuya dependencia nunca registraste. Los equipos resuelven esto con invalidación basada en etiquetas (asociar los agregados cacheados con los ids que los alimentaron, e invalidar todos los agregados etiquetados con un id cuando ese id cambia) o simplemente aceptan un TTL corto en los agregados como red de seguridad.
#3. Invalidación basada en eventos
A mayor escala, el servicio dueño del dato no sabe —ni debería necesitar saber— cuál de una docena de otros servicios cacheó una copia derivada de él. En su lugar, publica un evento de dominio cada vez que algo cambia, y cada servicio interesado se suscribe e invalida su propia caché.
Esto desacopla a quien escribe de la caché de cada consumidor corriente abajo: el servicio de productos nunca necesita enterarse de que existe un servicio de recomendaciones. El compromiso es que la invalidación ahora ocurre con un retraso determinado por el bus de eventos (típicamente milisegundos, pero no cero), y los consumidores deben manejar eventos que llegan desordenados o más de una vez. Un manejador de invalidación idempotente (invalidar es idempotente por naturaleza: borrar una clave ya borrada es un no-op) hace que los duplicados sean inofensivos, pero la entrega desordenada (un evento antiguo de "el precio cambió a $20" que llega después de uno más nuevo de "el precio cambió a $18") puede dejar una caché apuntando a un valor más viejo de lo que un manejador ingenuo cree actual, a menos que los eventos lleven una versión o marca de tiempo que el manejador verifique antes de aplicarlos.
#La condición de carrera que ningún diagrama muestra
Incluso una configuración de invalidación explícita "correcta según el libro" esconde un bug clásico:
Hilo A (lector, cache miss) Hilo B (escritor)------------------------------ -------------------1. GET product:1001 -> miss2. UPDATE precio = 25 en la BD3. DEL product:10014. SELECT precio FROM products (A ya leyó el precio VIEJO-> retorna 20 (la lectura de A antes del commit de B, desdeempezó antes de que B confirmara, una instantánea/nivel decon un snapshot anterior) aislamiento previo a él)5. SET product:1001 = 20 (TTL 60s)
El hilo A repuebla la caché con el valor obsoleto después de que el hilo B ya la había invalidado, porque la lectura de A en la base de datos corrió en paralelo con la escritura de B. Ahora la caché sirve confiadamente 20 por hasta otros 60 segundos aunque el precio correcto sea 25, y nada en el código se ve mal si lo miras aislado.
#Eligiendo una estrategia
| Estrategia | Ventana de desactualización | Complejidad | Buen encaje |
|---|---|---|---|
| Solo TTL | Hasta el TTL completo | La más baja | Datos que toleran estar algo desactualizados (catálogos de productos, configuración no crítica) |
| Invalidación explícita | Milisegundos, si se hace bien | Media: hay que rastrear cada clave derivada | Cachés de una sola entidad (perfil de usuario, registro de producto) |
| Basada en eventos | El retraso del bus de eventos | La más alta: exige idempotencia y cuidado con el orden | Arquitecturas multiservicio, cachés derivadas/agregadas |
🧠 Check your understanding
0/4 · 0/4 answered1. Una lectura cache-aside corre en paralelo con una escritura concurrente y repuebla la caché con un valor obsoleto justo después del DEL explícito del escritor. ¿Cuál es la red de seguridad estándar contra esta falla específica?
2. ¿Por qué es más difícil invalidar una caché de 'los 10 más vendidos esta semana' que la de un solo producto?
3. ¿Qué problema resuelve agregar jitter a un TTL (por ejemplo, 60 + random(0,10) segundos)?
4. En la invalidación basada en eventos entre servicios, ¿por qué los manejadores deben ser idempotentes?