¿Cómo mantienes un caché consistente cuando tienes múltiples instancias de la aplicación escribiendo en él?
Cuando varias instancias escriben el mismo dato, solo una invalidación disciplinada —no copiar el valor nuevo por todas partes— mantiene sincronizado cada caché.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo mantienes un caché consistente cuando tienes múltiples instancias de la aplicación escribiendo en él?
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 actualizar el caché" no escala más allá de una instancia
Cuando un solo proceso posee tanto el caché como las escrituras, mantenerlos sincronizados es trivial: actualizas el valor, actualizas el caché, listo. En el momento en que escalas horizontalmente a varias instancias de la aplicación —el caso normal para cualquier cosa detrás de un balanceador de carga— esa simplicidad desaparece. Ahora varios procesos pueden escribir la misma clave casi al mismo tiempo, en un orden que no tiene nada que ver con el orden en que sus escrituras realmente llegan a la base de datos, y cada uno de ellos puede además mantener una copia privada en proceso (L1) que ningún otro puede ver ni invalidar directamente.
El modo de fallo casi nunca es "el caché queda mal para siempre". Es una obsolescencia transitoria que aparece como bugs confusos y difíciles de reproducir: un usuario cambia de plan, recarga la página y sigue viendo el plan anterior durante unos segundos —pero solo a veces, y solo en algunos pods.
#Prefiere invalidar en vez de replicar
La decisión de diseño más importante es: en una escritura, elimina la entrada del caché en lugar de fijarla al nuevo valor. Fijar el caché en cada escritura resulta tentador porque evita un miss adicional, pero introduce una carrera fácil de perder:
- La instancia A empieza a escribir v2 en la base de datos.
- La instancia B empieza a escribir v3 (un momento después, legítimamente más nueva).
- La escritura de B confirma primero; B fija caché = v3.
- La escritura de A confirma después (por ejemplo, se retrasó por una réplica lenta o un reintento); A fija caché = v2.
- El caché queda con v2 —obsoleto— aunque la base de datos tiene correctamente v3.
Eliminar la clave en lugar de fijarla elimina casi por completo esta carrera: sea cual sea la escritura que termine última, el caché queda vacío, y el siguiente lector lo repuebla desde la base de datos, que es el único lugar con una vista consistente de "el valor más reciente". Un caché vacío nunca está mal; un caché con el valor equivocado sí.
#La única carrera que sobrevive: eliminar, luego repoblar
Incluso "eliminar en la escritura" tiene una ventana estrecha: un lector puede colarse entre tu escritura y tu eliminación, leer el valor antiguo de la base de datos (de una réplica con retraso, por ejemplo) y repoblar el caché con él justo después de que eliminaste la clave —dejando datos obsoletos indefinidamente, o hasta que expire el TTL. La mitigación estándar es el doble delete con retraso: eliminas la clave, haces la escritura, y eliminas la clave otra vez tras un breve retraso (suficiente para cubrir el retraso típico de replicación o lecturas en vuelo, por ejemplo 500 ms a unos pocos segundos). No hace el sistema linealizable, pero cierra la ventana realista sin añadir un lock distribuido a cada ruta de escritura.
#Propagar la invalidación a cada instancia
Eliminar el L2 compartido (Redis, Memcached) resuelve el caché compartido. No hace nada por los cachés L1 privados que viven dentro de cada proceso de la aplicación —esos siguen con el valor antiguo hasta que expire su propio TTL, porque nadie les avisó de lo contrario. Para eso sirve un canal de invalidación por pub/sub: la instancia que realiza la escritura publica un pequeño mensaje "esta clave cambió", y todas las instancias (incluida la que escribió, por uniformidad) están suscritas y desalojan esa clave de su L1 local en cuanto llega el mensaje.
Redis te da este mecanismo de forma nativa: un canal simple de PUBLISH/SUBSCRIBE al que escucha cada instancia, o —desde Redis 6— client-side caching mediante CLIENT TRACKING, donde el propio Redis recuerda qué cliente cacheó qué clave y empuja un mensaje de invalidación automáticamente cuando esa clave cambia, sin que tengas que publicarlo tú mismo. En ambos casos, la forma es la misma: un canal de difusión compartido que convierte N cachés independientes y ajenos entre sí en N cachés que concuerdan dentro de un round-trip de red.
#Cuidado con las estampidas tras una invalidación masiva
Si invalidas una clave caliente en todas las instancias a la vez —el precio de un producto popular, un flag de configuración que todos leen— puedes crear el problema inverso: todas las instancias hacen miss en el mismo instante y golpean la base de datos simultáneamente. Protégete con coalescencia de peticiones (una sola búsqueda en vuelo por clave, con quienes llegan después esperando su resultado en lugar de lanzar cada uno su propia consulta) o un lock de corta duración alrededor del paso de repoblar.
#Una guía rápida de decisión
| Mecanismo | Resuelve | No resuelve |
|---|---|---|
| Solo TTL | Acota la obsolescencia en el peor caso | Puede dejar una lectura obsoleta durante toda la ventana del TTL |
| Eliminar en la escritura (caché compartido) | Elimina las carreras de last-write-wins en L2 | Las copias privadas L1 de cada instancia |
| Invalidación por Pub/Sub | Propaga "esto cambió" a los L1 de todas las instancias casi al instante | La entrega no está garantizada — combínalo con un TTL |
| Doble delete con retraso | Cierra la carrera escritura/lectura con una réplica retrasada | Añade latencia/complejidad a cada ruta de escritura |
| Lock distribuido al repoblar | Evita una estampida tras una invalidación masiva | Añade un modo de fallo (el que tiene el lock puede caerse) si no se construye con cuidado |
🧠 Check your understanding
0/2 · 0/2 answered1. En una escritura, ¿qué opción es generalmente más segura para mantener correcto un caché compartido?
2. ¿Por qué un canal de invalidación por pub/sub no basta por sí solo para garantizar la corrección del caché?