Cache-aside vs. write-through vs. write-behind: ¿en qué se diferencian realmente estos patrones de caché?
Tres formas de mantener sincronizados una caché y una base de datos, cada una con un compromiso distinto entre latencia, consistencia y el radio de daño si algo falla.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Cache-aside vs. write-through vs. write-behind: ¿en qué se diferencian realmente estos patrones de caché?
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.#Las tres formas de "mantener sincronizada una caché con una base de datos"
Toda estrategia de caché responde dos preguntas: quién es responsable de cargar los datos en la caché y quién es responsable de propagar los cambios hacia la fuente de verdad. Las tres respuestas canónicas —cache-aside, write-through y write-behind— suenan a detalle de implementación hasta que un nodo de caché muere en medio de una escritura y tienes que explicarle al equipo por qué algunas filas quedaron desactualizadas, o simplemente desaparecieron.
#Cache-aside (o "lazy loading")
La aplicación es dueña explícita de la lógica de caché. En una lectura, primero consulta la caché; si falla (miss), carga desde la base de datos y llena la caché para la próxima vez. En una escritura, la versión más simple escribe directamente en la base de datos e invalida (borra) la clave en caché, dejando que la próxima lectura la repueble.
def obtener_usuario(user_id):en_cache = redis.get(f"user:{user_id}")if en_cache is not None:return json.loads(en_cache)fila = db.query("SELECT * FROM users WHERE id = %s", user_id)redis.setex(f"user:{user_id}", 300, json.dumps(fila))return filadef actualizar_usuario(user_id, campos):db.execute("UPDATE users SET ... WHERE id = %s", user_id)redis.delete(f"user:{user_id}") # invalida, no actualiza
Cache-aside es la opción por defecto por una razón concreta: la caché es desechable. Si Redis cae por completo, la aplicación se degrada a golpear la base de datos directamente (más lento, pero correcto) en lugar de servir datos incorrectos o caerse. Su debilidad es la estampida de lecturas ante un miss (muchas peticiones golpean la base de datos a la vez) y el hecho de que las escrituras dejan una breve ventana en la que la caché está vacía en vez de fresca: quien lea primero paga el costo de la recarga.
#Write-through
Aquí la caché está en el camino de escritura, no solo en el de lectura. La aplicación escribe en la caché, y la caché (o una capa delgada alrededor de ella) escribe de forma síncrona en la base de datos antes de que la llamada retorne. Cada escritura queda reflejada de inmediato en ambos lugares.
def actualizar_usuario_write_through(user_id, campos):fila = db.execute_and_return("UPDATE users SET ... WHERE id = %s RETURNING *", user_id)redis.setex(f"user:{user_id}", 300, json.dumps(fila)) # se refresca la caché, no solo se invalidareturn fila
Las lecturas son siempre aciertos de caché después de la primera escritura, y la caché nunca sirve datos obsoletos respecto a la última escritura conocida —asumiendo que nada más escribe directamente en la base de datos. El costo es la latencia de escritura: cada escritura ahora paga el viaje de ida y vuelta a la base de datos y a la caché, de forma síncrona. También significa que estás poblando la caché con cosas que quizás nunca se vuelvan a leer, lo cual puede desperdiciar memoria en datos que se escriben mucho pero se leen poco.
#Write-behind (o write-back)
La aplicación escribe solo en la caché, que confirma de inmediato, y un proceso en segundo plano vuelca el cambio hacia la base de datos de forma asíncrona, a menudo en lotes, a menudo a través de una cola.
def actualizar_usuario_write_behind(user_id, campos):fila = aplicar_campos_en_memoria(user_id, campos)redis.setex(f"user:{user_id}", 300, json.dumps(fila))cola_escritura.push({"op": "update_user", "id": user_id, "campos": campos})return fila # quien llama ve éxito antes de que ocurra la escritura en BD# proceso worker separadodef flush_worker():lote = cola_escritura.pop_batch(max_size=100)db.execute_many("UPDATE users SET ... WHERE id = %s", lote)
La latencia de escritura baja a "lo rápido que sea la caché", y agrupar muchas escrituras en menos viajes a la base de datos puede reducir drásticamente su carga: así es como los sistemas absorben picos de escritura. El compromiso es la durabilidad: si el nodo de caché o la cola mueren antes de que corra el volcado, esa escritura desaparece, y la base de datos queda en silencioso desacuerdo con lo que se le dijo a cada cliente que había tenido éxito.
#Comparando los tres
| Cache-aside | Write-through | Write-behind | |
|---|---|---|---|
| Quién carga en un miss de lectura | La aplicación | La aplicación (raro, la caché se mantiene caliente) | La aplicación |
| Quién escribe en la BD | La aplicación, directamente | La capa de caché, de forma síncrona | Un worker en segundo plano, de forma asíncrona |
| Latencia de escritura | Solo BD | BD + caché (mayor) | Solo caché (la más baja) |
| Riesgo si falla la caché | Cae a la BD, más lento | Cae a la BD, más lento | Pérdida de datos en escrituras no volcadas |
| Mejor para | Lecturas de propósito general | Consistencia de lectura tras escritura | Throughput de escritura muy alto |
La mayoría de los sistemas en producción no eligen un solo patrón de forma global: eligen por tipo de dato. Cache-aside para el grueso de las lecturas, write-through para datos que nunca deben verse obsoletos justo después de escribirse (por ejemplo, el propio perfil de un usuario, inmediatamente después de guardarlo), y write-behind reservado para escrituras de alto volumen y tolerantes a pérdida, como contadores de vistas o registros de actividad, donde perder los últimos incrementos es un precio aceptable por absorber un pico de tráfico.