MySQL vs Redis: Write-through vs Cache-aside — Qué falla menos en aplicaciones reales

¿Te fue útil?

Todo es rápido en staging. Luego la producción aparece con un percentil 99 que parece una pendiente y un clúster de Redis que está “bien” hasta que deja de estarlo. Alguien añade caché para arreglar un punto caliente de MySQL. El punto caliente se mueve. Entonces el pager se muda a tu dormitorio.

Esta es la verdad poco glamorosa: MySQL y Redis no son competidores en sistemas reales; son compañeros de trabajo. Tu tarea es evitar que discrepen silenciosamente sobre la realidad. Eso es lo que son write-through y cache-aside: contratos entre tu app, tu caché y tu base de datos. Un contrato falla menos—si lo eliges por las razones correctas y lo operas como corresponde.

La pregunta real: qué falla menos

“MySQL vs Redis” es un debate falso. MySQL es tu sistema de registro. Redis es tu apuesta por el rendimiento. La pregunta que importa es: ¿qué patrón de caché produce menos fallos visibles para el cliente bajo el caos operativo normal—despliegues, fallos parciales, picos de latencia y el ocasional “alguien ejecutó un comando en la terminal equivocada”.

Si quieres la opinión por adelantado:

  • Cache-aside falla menos para la mayoría de las aplicaciones de producto porque falla abierto: cuando Redis está malo, aún puedes leer y escribir en MySQL y seguir adelante a paso lento.
  • Write-through falla menos solo cuando puedes invertir en garantías operativas fuertes: timeouts disciplinados, encolamiento o reintentos con idempotencia y reglas claras sobre qué ocurre si Redis o MySQL no están disponibles.
  • Write-through falla de forma más ruidosa. Eso no siempre es malo. Los fallos ruidosos son depurables. La inconsistencia silenciosa es donde pierdes fines de semana.

No hay almuerzo gratis; solo hay lugares donde prefieres pagar. Cache-aside normalmente paga con lecturas ocasionalmente obsoletas y stampedes. Write-through paga con latencia en la vía de escritura y más formas de atrancar toda la app si tu caché sufre.

Una cita para mantener pegada al monitor, porque ha salvado muchas rotaciones de on-call (idea parafraseada): Werner Vogels: construye para el fallo—supón que todo falla y diseña para que no se convierta en una catástrofe.

Definiciones que puedes desplegar

MySQL

Base de datos relacional persistente. Almacenamiento duradero, semántica transaccional y flexibilidad de consulta. También: lo que todos culpan cuando la app está lenta, incluso cuando el problema es la red, el pool o el diseño de claves de caché que se volvió arte performance.

Redis

Almacén en memoria de estructuras de datos. Usado como caché, cola, limitador de tasas, store de sesiones y “base de datos temporal que prometemos que no es una base de datos”. Redis puede persistir (instantáneas RDB, logs AOF), puede replicar y puede clusterizar, pero sigue siendo una bestia distinta de MySQL: optimiza por velocidad y simplicidad, no por corrección relacional.

Cache-aside (carga perezosa)

La aplicación es responsable de las lecturas de caché y de rellenarla:

  1. Ruta de lectura: comprobar Redis → si falta, leer MySQL → escribir en Redis → devolver.
  2. Ruta de escritura: escribir en MySQL → invalidar o actualizar Redis.

Punto clave: la caché es opcional. Si Redis falla, puedes rodearlo y golpear MySQL. Tu peor día se convierte en “lento” en lugar de “caído”, si MySQL puede soportar la carga.

Write-through (población síncrona)

La aplicación escribe en caché y en la base de datos en la misma operación lógica. Existen variantes, pero el espíritu es:

  1. Ruta de escritura: escribir en Redis (o capa de caché) y MySQL como parte de una única petición.
  2. Ruta de lectura: leer desde Redis; se espera que la caché esté caliente y correcta.

Punto clave: la caché pasa a ser parte de la corrección. Si Redis está poco saludable, tu ruta de escritura se ve afectada. Si tu capa write-through miente, tu app miente.

Broma corta #1: Write-through es como una solicitud de cambio “rápida” en una gran empresa—rápida hasta que empiezan las aprobaciones.

Hechos e historia que realmente importan

  • Redis nació (2009) como respuesta práctica al acceso lento de datos en apps web, no como una plataforma unificada grandiosa. Su ADN de “hacer cosas simples extremadamente rápido” todavía se nota.
  • Memcached popularizó el cache-aside en el mainstream. Muchas prácticas de caché con Redis se heredaron de esa era: TTLs por todas partes, invalidación best-effort y tolerancia a la inconsistencia ocasional.
  • El query cache de MySQL fue eliminado (MySQL 8.0) porque causaba contención y rendimiento impredecible. El caching se movió hacia capas de aplicación y caches dedicados.
  • Redis es single-threaded para la ejecución de comandos (con algo de I/O multihilo en versiones nuevas). Por eso es rápido y predecible—hasta que ejecutas comandos lentos y bloqueas el mundo.
  • La persistencia de Redis es opcional y configurable: las snapshots RDB intercambian durabilidad por velocidad; AOF intercambia sobrecarga de escritura en disco por mejor granularidad de recuperación. La elección cambia lo que incluso significa “write-through”.
  • La replicación es asíncrona por defecto en Redis y en muchas topologías MySQL. “Lo escribí” puede significar “el primario lo aceptó”, no “está seguro frente a fallos”.
  • El stampede de caché (thundering herd) es un problema conocido desde hace décadas en sistemas web grandes; mitigaciones como coalescencia de peticiones y TTLs con jitter son ideas antiguas—todavía ignoradas semanalmente.
  • Redis Cluster hace sharding por hash slot. Las operaciones multi-clave se complican rápido, y las operaciones cross-slot pueden convertirse en trampas silenciosas para workflows write-through.

Write-through vs cache-aside: comparación para tomar decisiones

Qué estás optimizando

Si tu dolor principal es la latencia de lectura y tu conjunto de datos es relativamente estable, cache-aside suele ser suficiente. Si tu dolor principal es la amplificación de lecturas causada por objetos computados complejos (p. ej., perfil de usuario ensamblado + permisos + contadores) y quieres una caché consistentemente caliente, write-through empieza a verse atractivo.

Pero no confundas “caliente” con “correcto”. Caliente es fácil. Correcto es donde llegan las facturas.

Radio de explosión operativo

  • Cache-aside: caída de Redis → mayor carga en MySQL → posible saturación de MySQL → lentitud u outage parcial. Puedes degradar de forma gradual si lo preparaste.
  • Write-through: caída de Redis → tu ruta de escritura puede bloquearse o fallar → fallos en cascada en la capa de app → outage aunque MySQL esté bien.

Perfil de consistencia

Ningún patrón te da consistencia transaccional entre MySQL y Redis sin maquinaria extra. La elección es qué inconsistencia prefieres:

  • Cache-aside arriesga lecturas obsoletas tras escrituras (carreras de invalidación, lag de replicación, deletes perdidos).
  • Write-through arriesga verdad en conflicto si una escritura tiene éxito y la otra falla o los reintentos reordenan operaciones. No es obsoleto; es contradictorio.

Perfil de latencia

Cache-aside mantiene la vía de escritura mayormente limitada por MySQL, que ya afinaste. Write-through añade Redis a la vía de escritura. Si Redis está en otra zona, detrás de TLS o simplemente ocupado, felicidades: acabas de convertir un problema de disco local en un problema de sistemas distribuidos.

Cuándo elijo cache-aside

  • Cargas con muchas lecturas y consistencia eventual tolerable.
  • Objetos que se pueden regenerar desde MySQL a demanda.
  • Equipos que quieren una caché que pueda ser evitada durante incidentes.
  • Modelos de datos con escrituras frecuentes e invalidación compleja pueden funcionar—si mantienes las reglas simples.

Cuándo elijo write-through

  • Tienes una capa/servicio de caché clara que posee el contrato write-through y puede operarse como un componente de base de datos.
  • Puedes imponer idempotencia y orden para las escrituras (o tolerar last-write-wins).
  • Estás dispuesto a presupuestar latencia y disponibilidad para Redis como infraestructura de ruta crítica.
  • Quieres predictibilidad de caché caliente para lecturas y puedes mantener estructuras de datos sencillas.

Modos de fallo: cómo muere cada patrón

Cache-aside: los clásicos

1) Lecturas obsoletas por carreras de invalidación

Secuencia típica:

  1. La petición A lee, cache miss, obtiene fila de MySQL (valor viejo), luego se dispone a poner en Redis.
  2. La petición B actualiza la fila en MySQL (valor nuevo), borra la clave en Redis.
  3. La petición A ahora escribe en Redis con el valor viejo tras el DELETE de B.

Resultado: Redis contiene datos obsoletos hasta que expire el TTL o haya otra invalidación. Los clientes ven estado viejo. Los ingenieros oyen “pero borramos la clave”. Ambas cosas son verdad.

Mitigación: claves versionadas, compare-and-set (script Lua con versión) o escribir actualizaciones en caché con una versión/timestamp monótonamente creciente.

2) Stampede de caché tras expiración

Una clave caliente expira. Miles de peticiones hacen miss a la vez. Todas golpean MySQL. MySQL se cae. Redis observa el drama como un gato viendo un puntero láser.

Mitigación: coalescencia de peticiones (single flight), refresh probabilístico anticipado, mutex por clave, TTLs con jitter y “servir obsoleto mientras se vuelve a validar”.

3) Penetración de caché (misses para claves inexistentes)

Tráfico malicioso o clientes con bugs piden IDs que no existen. Los misses de caché no se almacenan, de modo que MySQL recibe consultas inútiles.

Mitigación: negative caching con TTLs cortos, filtros Bloom, límites de tasa.

4) Fallos parciales silenciosos

Los timeouts de Redis no se tratan como fallos. La app espera demasiado y consume hilos. O la app reintenta agresivamente y se convierte en DoS.

Mitigación: timeouts estrictos, circuit breakers y semántica clara de “la caché es best-effort”.

Write-through: menos misses, más trampas de corrección

1) Inconsistencia por doble escritura

Escribir en Redis tiene éxito; escribir en MySQL falla. O viceversa. O ambos tienen éxito pero reintentos reordenan operaciones. Ahora tu caché y tu BD discrepan, y tu ruta de lectura está garantizada para servir algo—posiblemente incorrecto.

Mitigación: outbox transaccional, change-data-capture (CDC) para impulsar actualizaciones de caché, o hacer que MySQL sea la autoridad y tratar las escrituras en caché como derivadas.

2) Amplificación de latencia en la vía de escritura

Los picos de Redis (fsync de AOF lento, jitter de red, picos de CPU). Write-through convierte eso en latencia visible para el usuario. Los timeouts causan reintentos. Los reintentos causan carga. La carga causa más timeouts. Ya conoces el resto.

3) Sorpresas por la persistencia de Redis

Si confías en Redis como parte de la corrección write-through, pero Redis está configurado solo con snapshotting, un crash puede perder escrituras recientes. MySQL puede estar correcto; Redis puede estar en el pasado. Si tu app lee Redis primero, servirás viajes en el tiempo.

4) Topología de cluster y trampas de diseño de claves

Write-through suele querer atomicidad multi-clave (actualizar objeto + índice + contadores). Redis Cluster no puede ejecutar transacciones multi-clave a través de hash slots. La gente lo evita con hash tags y luego descubre que construyeron un shard hotspot.

Broma corta #2: Invalidación de caché es uno de los problemas difíciles, pero al menos no tiene reuniones. Las dobles escrituras sí.

Tres mini-historias corporativas desde la trinchera

Incidente #1: el outage provocado por una suposición equivocada

Una compañía SaaS mediana ejecutaba un setup clásico de cache-aside: primario MySQL con réplicas, Redis para objetos calientes. El equipo asumió “las lecturas a Redis son baratas, así que podemos usarlas por todas partes”. Espolvorearon llamadas a Redis por todo el código—feature flags, límites de tasa, sesiones de usuario y algunas comprobaciones críticas de autorización.

Entonces llegó un problema de red regional que aumentó la pérdida de paquetes entre la capa de app y los nodos Redis. Redis en sí estaba sano. La latencia ni siquiera era terrible—solo con jitter y timeouts. El cliente de Redis de la app tenía un timeout por defecto de 2 segundos y un reintento. Bajo carga, los hilos se acumularon esperando Redis. Finalmente los workers web llegaron a la concurrencia máxima y dejaron de aceptar peticiones.

MySQL estaba bien. La CPU estaba bien. El comandante del incidente seguía oyendo “pero Redis está arriba”. Claro. Un servidor puede estar “arriba” de la misma forma que una puerta puede estar “cerrada”. Ambas son técnicamente verdad y operativamente inútiles.

La solución no fue heroica. Redujeron los timeouts de Redis a algo que reflejara la realidad (decenas de milisegundos, no segundos), añadieron un circuit breaker y—esto es clave—dejaron de usar Redis como dependencia rígida para decisiones de autorización. Para auth, cachearon en proceso con TTLs cortos y recurrieron a MySQL cuando era necesario. Redis volvió a ser una caché, no un juez.

Incidente #2: la optimización que salió mal

Una plataforma de marketplace quería lecturas de perfil más rápidas. Construyeron un flujo write-through: cuando un usuario actualizaba su perfil, el servicio escribía el blob desnormalizado en Redis y luego actualizaba MySQL. Las lecturas eran Redis-first, sin fallback a MySQL salvo en “mantenimiento”.

Funcionó hermoso hasta que un deploy introdujo un bug sutil: los reintentos en fallos de escritura a MySQL no eran idempotentes. El código añadía nuevas preferencias en vez de reemplazarlas, y la escritura en caché ocurría antes de la escritura en MySQL. Bajo un breve evento de contención de locks en MySQL, el servicio reintentó. Redis ahora tenía el blob más nuevo (con entradas de preferencia duplicadas), mientras que MySQL tenía una mezcla de filas viejas y parcialmente actualizadas según qué reintento hubiese triunfado.

Los clientes vieron perfiles inconsistentes según qué instancia de servicio los atendiera y qué clave de caché encontraran. Soporte describió el problema como “las configuraciones no se guardan”. Los ingenieros dijeron “no tenemos idea de cuál sistema tiene la razón”. La caché se había convertido en una segunda fuente de verdad, sin la madurez operativa de una base de datos.

El rollback restauró algo de cordura, pero la reparación real tomó más tiempo: pasaron a escrituras autoritarias en MySQL y usaron un stream de cambios para actualizar Redis después del commit. También añadieron un campo de versión y rechazaron escrituras de caché más viejas. La optimización había sido más rápida. También era mentirosa.

Incidente #3: la práctica aburrida pero correcta que salvó el día

Un servicio adyacente a pagos (no el ledger central, pero lo bastante sensible) usaba cache-aside con reglas estrictas: las caches podían estar obsoletas, pero nunca se usaban para decisiones de balance final. Cada clave de caché tenía TTL, versión y un propietario. Cada llamada a Redis tenía un timeout ajustado y una estrategia de fallback documentada en un runbook.

Una tarde, un failover de Redis (triggered por Sentinel) provocó una breve ventana donde algunos clientes escribieron al viejo primario y otros al nuevo. Esto por sí no fue catastrófico; era el tipo de caos que deberías esperar. Su app lo manejó porque trataron Redis como best effort. Las escrituras fueron a MySQL; las invalidaciones de caché se intentaron pero no eran necesarias para la corrección.

El sistema se ralentizó. Saltaron alertas. Pero el servicio se mantuvo en pie y el límite de corrección permaneció intacto. El on-call siguió el runbook: deshabilitó temporalmente las lecturas de caché para los endpoints más calientes, dejó que MySQL gestionara lecturas por un tiempo y observó cómo las tasas de error se estabilizaban.

No hubo heroísmos. No hubo algoritmos nuevos. Solo contratos claros y la voluntad de aceptar un golpe temporal de rendimiento a cambio de no corromper el estado. Esa disciplina “aburrida” es lo que la gente quiere decir cuando dice que la fiabilidad es una característica.

Guía rápida de diagnóstico

Cuando las cosas se ponen lentas o inconsistentes, no tienes tiempo para filosofía. Necesitas una secuencia corta que identifique el cuello de botella y el dominio del fallo.

Primero: decide si es el camino Redis, MySQL o la app

  1. Revisa los síntomas visibles por el usuario: ¿las lecturas están lentas, las escrituras lentas, o ambas? ¿Los errores son timeouts o desajustes de datos?
  2. Revisa latencia y saturación de Redis: picos instantáneos de latencia, clientes bloqueados, evictions.
  3. Revisa concurrencia en MySQL: consultas en ejecución, esperas por locks, lag de replicación.
  4. Revisa salud del pool de la app: hilos/pools de conexión, profundidad de colas, pausas de GC.

Segundo: prueba caminos de bypass

  • Si puedes evitar de forma segura las lecturas a Redis para un endpoint caliente, hazlo y observa si la latencia vuelve a la normalidad. Si lo hace, el problema está en el camino Redis (o en la librería cliente).
  • Si puedes evitar réplicas MySQL y leer del primario (brevemente), hazlo para validar lag de replicación.

Tercero: valida el límite de corrección

  • Elige un usuario/objeto con una actualización reciente conocida y compara directamente valores entre MySQL y Redis.
  • Si difieren, descubre si tu patrón puede producir esa diferencia (carrera de invalidación vs fallo de doble escritura) y procede en consecuencia.

Tareas prácticas: comandos, salidas y decisiones

Estos no son comandos de juguete. Son los que ejecutas a las 2 a.m. para dejar de adivinar. Cada tarea incluye qué significa la salida y la decisión que tomas a partir de ella.

Task 1: ¿Responde Redis rápidamente desde el host de la app?

cr0x@server:~$ redis-cli -h redis-01 -p 6379 --latency -i 1
min: 0, max: 2, avg: 0.31 (1000 samples)
min: 0, max: 85, avg: 1.12 (1000 samples)

Significado: La segunda línea muestra picos ocasionales de 85ms. No es fatal, pero si tu timeout de app es 50ms, se convierte en error.

Decisión: Si max/avg está por encima de tu SLO, investiga CPU de Redis, settings de fsync de persistencia, jitter de red o comandos lentos. Considera relajar temporalmente la dependencia de caché (fallback) si estás en write-through.

Task 2: ¿Se están acumulando clientes en Redis?

cr0x@server:~$ redis-cli -h redis-01 INFO clients | egrep 'connected_clients|blocked_clients'
connected_clients:1248
blocked_clients:37

Significado: blocked_clients > 0 suele indicar consumidores BLPOP/BRPOP, scripts Lua o clientes esperando algo que no sucede. En caches, clientes bloqueados suelen ser señal de problemas.

Decisión: Identifica comandos bloqueantes, revisa scripts Lua lentos o transacciones atascadas. Si blocked_clients sube con la latencia, trátalo como degradación de servicio y reduce la carga de caché.

Task 3: ¿Redis está evictando claves (presión de memoria)?

cr0x@server:~$ redis-cli -h redis-01 INFO stats | egrep 'evicted_keys|keyspace_hits|keyspace_misses'
keyspace_hits:93811233
keyspace_misses:12100444
evicted_keys:482919

Significado: Las evictions significan que tu caché ya no es caché; es una máquina de churn. Altos misses amplifican la carga en MySQL. Las evictions también destruyen cualquier suposición de “caché caliente” en write-through.

Decisión: Aumenta maxmemory, corrige TTLs, reduce tamaños de valores, mejora distribución de claves o cambia política de eviction. En un incidente: deshabilita caché para endpoints de bajo valor para reducir churn.

Task 4: ¿Qué política de eviction está configurada?

cr0x@server:~$ redis-cli -h redis-01 CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lru"

Significado: allkeys-lru evapora cualquier clave bajo presión. Si guardas sesiones/locks junto a entradas de caché, también serán evicted. Así se producen desconexiones aleatorias y bugs de “¿por qué se ejecutó el job dos veces?”.

Decisión: Separa claves críticas en otra instancia/db de Redis o usa políticas como volatile-ttl para caches puras. No mezcles datos que “no deben desaparecer” con datos best-effort.

Task 5: ¿Comandos lentos están bloqueando Redis?

cr0x@server:~$ redis-cli -h redis-01 SLOWLOG GET 5
1) 1) (integer) 912341
   2) (integer) 1766812230
   3) (integer) 58321
   4) 1) "ZRANGE"
      2) "leaderboard"
      3) "0"
      4) "50000"
      5) "WITHSCORES"
   5) "10.21.4.19:51722"
   6) ""

Significado: Un ZRANGE de 58ms que devuelve 50k elementos bloqueará el event loop. Redis es rápido, pero no es milagroso. Respuestas grandes son costosas.

Decisión: Limita rangos, pagina o rediseña el acceso a datos. Si es una caché, los sorted sets grandes suelen ser requisitos producto accidentales disfrazados de técnicos.

Task 6: ¿La persistencia de Redis está causando latencia de escritura?

cr0x@server:~$ redis-cli -h redis-01 INFO persistence | egrep 'aof_enabled|aof_last_write_status|rdb_bgsave_in_progress'
aof_enabled:1
aof_last_write_status:ok
rdb_bgsave_in_progress:0

Significado: AOF está habilitado. Si el disco es lento o fsync es agresivo, la latencia de escritura puede dispararse, especialmente doloroso en write-through.

Decisión: Para caché pura, considera deshabilitar AOF o usar una política de fsync menos estricta. Para usos cercanos a la corrección, mide latencia de disco y asegúrate de que la persistencia coincida con tu modelo de riesgo.

Task 7: ¿MySQL está saturado o esperando locks?

cr0x@server:~$ mysql -h mysql-01 -e "SHOW PROCESSLIST" | head
Id	User	Host	db	Command	Time	State	Info
4123	app	10.21.5.11:53312	prod	Query	12	Waiting for table metadata lock	UPDATE users SET ...
4188	app	10.21.5.18:50221	prod	Query	9	Sending data	SELECT ...

Significado: Esperas por metadata locks sugieren DDL o cambios de esquema que colisionan con tráfico. Cache-aside no te salvará si las escrituras están atascadas por locks.

Decisión: Para, haz rollback del DDL o muévelo fuera de horario con herramientas de cambio de esquema online. Mientras tanto, reduce concurrencia de escrituras o deshabilita features que golpeen las tablas bloqueadas.

Task 8: ¿Qué dice InnoDB que está pasando ahora mismo?

cr0x@server:~$ mysql -h mysql-01 -e "SHOW ENGINE INNODB STATUS\G" | egrep -i 'LATEST DETECTED DEADLOCK|Mutex spin waits|history list length' | head -n 20
LATEST DETECTED DEADLOCK
Mutex spin waits 0, rounds 0, OS waits 0
History list length 987654

Significado: Una large history list length puede indicar lag de purge por transacciones largas, causando bloat y peor rendimiento. Deadlocks pueden mostrar patrones de contención en escrituras.

Decisión: Identifica transacciones de larga duración, corrige el scope de transacción en la app o ajusta aislamiento e índices. Si las invalidaciones cache-aside dependen de estas escrituras, también se acumularán.

Task 9: ¿El lag de replicación está causando lecturas obsoletas (acusadas como caché)?

cr0x@server:~$ mysql -h mysql-replica-01 -e "SHOW REPLICA STATUS\G" | egrep 'Seconds_Behind_Source|Replica_IO_Running|Replica_SQL_Running'
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 47

Significado: 47 segundos de lag se verá exactamente como “obsolescencia de caché” si tu app lee desde réplicas. Invalidarás la caché y aun así leerás datos viejos.

Decisión: Dirige tráfico read-after-write al primario (o usa consistencia de lectura basada en GTID). Arregla cuellos de botella de replicación antes de reescribir la lógica de caching.

Task 10: ¿Están expirando las claves de Redis en una onda sincronizada?

cr0x@server:~$ redis-cli -h redis-01 --scan --pattern 'user:*' | head -n 5
user:10811
user:10812
user:10813
user:10814
user:10815

Significado: Estás muestreando el keyspace. Si la mayoría de claves comparten TTLs idénticos (verificarás a continuación), te estás preparando para stampedes.

Decisión: Añade jitter al TTL (offset aleatorio) o implementa refresh anticipado y locks single-flight para claves calientes.

Task 11: Comprobar distribución de TTL para una clave caliente

cr0x@server:~$ redis-cli -h redis-01 TTL user:10811
(integer) 60

Significado: Un TTL limpio de 60 segundos es sospechosamente sincronizado si se aplica ampliamente. Muchas apps hacen exactamente esto y luego se preguntan por qué la BD se cae cada minuto.

Decisión: Cámbialo a algo como 60±15 segundos de jitter, o usa soft TTL (servir obsoleto mientras se revalida).

Task 12: Validar una inconsistencia sospechada (Redis vs MySQL)

cr0x@server:~$ redis-cli -h redis-01 GET user:10811
{"id":10811,"email":"old@example.com","version":17}
cr0x@server:~$ mysql -h mysql-01 -e "SELECT id,email,version FROM users WHERE id=10811"
id	email	version
10811	new@example.com	18

Significado: Redis está atrasado. Con cache-aside, esto podría ser una carrera de invalidación o un delete perdido. Con write-through, podría ser un fallo parcial de doble escritura o un bug de reordenamiento por replay.

Decisión: Si es cache-aside: borra la clave y audita caminos de invalidación, añade escrituras versionadas. Si es write-through: trátalo como incidente de corrección—detén la línea, encuentra la ruta de fallo de doble escritura, considera mover las actualizaciones a Redis para que las impulse la BD.

Task 13: Revisa salud de replicación de Redis (si ejecutas réplicas)

cr0x@server:~$ redis-cli -h redis-01 INFO replication | egrep 'role|connected_slaves|master_link_status|master_last_io_seconds_ago'
role:master
connected_slaves:1
master_link_status:up
master_last_io_seconds_ago:1

Significado: Si master_link_status está down o last_io es alto, puede haber un failover en progreso o réplicas desactualizadas. En write-through con lecturas desde réplicas, eso es una trampa de corrección.

Decisión: Prefiere leer del master para una consistencia más fuerte, o acepta consistencia eventual y trata lecturas obsoletas como esperadas. No finjas que tienes ambas.

Task 14: Revisa dolor de red a nivel Linux entre app y Redis/MySQL

cr0x@server:~$ ss -tan state established '( dport = :6379 or dport = :3306 )' | wc -l
842

Significado: Alto conteo de conexiones establecidas puede indicar falta de pooling, churn de conexiones o sockets atascados. Esto se vuelve latencia y luego timeouts.

Decisión: Asegura que la app poolée conexiones, ajusta límites de cliente y verifica maxclients de Redis / max_connections de MySQL. En incidentes, limita concurrencia en el ingreso.

Task 15: Revisa memoria y fragmentación de Redis

cr0x@server:~$ redis-cli -h redis-01 INFO memory | egrep 'used_memory_human|used_memory_rss_human|mem_fragmentation_ratio'
used_memory_human:12.31G
used_memory_rss_human:16.02G
mem_fragmentation_ratio:1.30

Significado: Fragmentation ratio 1.30 sugiere overhead del allocator/fragmentación. No siempre es fatal, pero reduce el tamaño efectivo de la caché y puede disparar evictions.

Decisión: Si estás limitado por evictions, considera reiniciar durante una ventana de mantenimiento, habilitar active defrag o redimensionar memoria. También reduce el churn de valores.

Errores comunes (síntoma → causa raíz → solución)

1) “Redis está arriba pero el sitio está caído”

Síntoma: Alta latencia de peticiones y timeouts; Redis muestra CPU y memoria saludables.

Causa raíz: Timeouts en cliente demasiado altos + reintentos + agotamiento del thread pool. Redis está “arriba”, pero tu app está atascada esperando.

Solución: Establece timeouts agresivos (típicamente 5–50ms según topología), limita reintentos, añade circuit breaker, asegura fallback a MySQL para lecturas cache-aside.

2) “Invalidamos la caché y sigue estando obsoleta”

Síntoma: Tras actualizaciones, algunos usuarios ven datos viejos durante minutos.

Causa raíz: Carrera de invalidación (delete y luego refill con valor viejo), o lag de réplicas (lees datos viejos de una réplica y repueblas la caché).

Solución: Usa claves versionadas o escrituras CAS; dirige read-after-write al primario o aplica consistencia de lectura; añade TTL jitter y single-flight.

3) “Las escrituras se volvieron más lentas después de añadir caché”

Síntoma: Aumento en P95 de latencia de escritura; aparecen timeouts en endpoints de actualización.

Causa raíz: Write-through añadió Redis a la ruta crítica; la persistencia fsync o el jitter de red amplifican la latencia.

Solución: Haz las escrituras en caché asíncronas (DB-autoritario + CDC), o acepta cache-aside con invalidación; ajusta persistencia de Redis o aísla un Redis solo para caché.

4) “Cierres de sesión aleatorios, jobs duplicados, locks faltantes”

Síntoma: Sesiones desaparecen; jobs background se ejecutan dos veces; locks distribuidos fallan.

Causa raíz: Usar una sola instancia de Redis para claves de caché volátiles y datos efímeros críticos; la política de eviction elimina claves importantes.

Solución: Separa instancias de Redis o al menos presupuestos/políticas de memoria; evita eviction para datos de coordinación; monitoriza evicted_keys.

5) “Cada minuto la base de datos se derrite”

Síntoma: Picos periódicos en QPS y latencia de MySQL, alineados con límites de TTL.

Causa raíz: Expiración sincronizada de TTLs en claves calientes; stampede.

Solución: TTL jitter, refresh anticipado, coalescencia de peticiones, servir obsoleto mientras se revalida y cache parcial de componentes costosos.

6) “La tasa de hits es alta pero el rendimiento sigue siendo malo”

Síntoma: La tasa de hits en Redis parece excelente; la app sigue lenta.

Causa raíz: Los valores grandes generan overhead de red; costos de serialización/deserialización; comandos lentos en Redis; CPU de la app saturada.

Solución: Mide tamaño de payloads, comprime selectivamente, guarda proyecciones más pequeñas, evita queries de rango pesadas, perfila CPU de la app.

7) “Tenemos corrupción de datos pero sin errores”

Síntoma: Usuarios ven estado contradictorio según el endpoint.

Causa raíz: Doble escritura sin idempotencia y sin garantías de orden; el diseño write-through asume simetría de éxito.

Solución: Para la doble escritura en la vía de petición. Usa outbox transaccional/CDC para actualizar la caché después del commit; añade checks de versión; define una única fuente de verdad.

Listas de verificación / plan paso a paso

Elige el patrón: un árbol de decisión práctico

  1. ¿Puede el sistema funcionar correctamente con Redis caído?
    • Sí → por defecto usa cache-aside.
    • No → estás construyendo un almacén distribuido. Trata a Redis como infraestructura crítica y considera si MySQL sigue siendo necesario en la ruta caliente.
  2. ¿Requieres consistencia read-after-write para flujos visibles al usuario?
    • Sí → cache-aside con lecturas al primario para la sesión, o actualizaciones de caché impulsadas por BD con versionado.
    • No → cache-aside con TTL + jitter suele ser suficiente.
  3. ¿Las escrituras son frecuentes y sensibles a la latencia?
    • Sí → evita write-through síncrono salvo que Redis esté extremadamente cercano y muy bien operado.
    • No → write-through puede ser aceptable si simplifica lecturas y puedes imponer idempotencia.

Runbook cache-aside: implementación “corrección primero”

  1. Define la fuente de verdad: MySQL es el autoritativo. Redis es derivado.
  2. Ruta de lectura: Redis GET → en miss, leer MySQL → set en Redis con TTL + jitter.
  3. Ruta de escritura: Escribir MySQL en transacción → después del commit, invalidar (DEL) o actualizar Redis.
  4. Prevenir stampedes: implementa single-flight por clave (mutex con TTL corto), o servir obsoleto mientras se refresca.
  5. Añadir versionado: embebe versión en el valor; rechaza escrituras de caché más viejas si puedes.
  6. Timeout corto: timeout de Redis corto; si se alcanza, omite caché y lee MySQL.
  7. Observabilidad: monitoriza hit rate, evictions, latencia y QPS de MySQL durante simulacros de bypass de caché.

Runbook write-through: si lo insistes, hazlo en serio

  1. Haz las escrituras idempotentes: los reintentos no deben crear estado nuevo. Usa IDs de petición, versiones o upserts con cuidado.
  2. Define orden: last-write-wins necesita un timestamp/versión monotónico por objeto.
  3. Planifica comportamiento en fallos parciales: si la escritura en Redis falla pero MySQL tiene éxito, ¿qué pasa? Si MySQL falla pero Redis tiene éxito, ¿cómo reparas?
  4. Prefiere actualizaciones de caché impulsadas por BD: commit en MySQL, luego actualiza Redis vía worker asíncrono consumiendo outbox/CDC.
  5. Presupuesta latencia: Redis entra en el SLO de escritura. Mide, alerta y planifica capacidad en consecuencia.
  6. Aislamiento: no compartas este Redis con caches best-effort y experimentos aleatorios de features.

Checklist de incidente: mantener el servicio vivo

  1. Reduce el radio de explosión: deshabilita lecturas de caché para los endpoints más calientes si es seguro.
  2. Limita concurrencia en el ingreso (shed load mejor que colapso total).
  3. Verifica evictions y latencia de Redis; verifica locks y lag de replicación en MySQL.
  4. Si la corrección está comprometida (inconsistencia por doble escritura), congela las escrituras o dirige lecturas a MySQL mientras reparas.
  5. Tras la estabilización, rellena la caché y ejecuta muestreos de consistencia.

Preguntas frecuentes

1) ¿Cuál falla menos: cache-aside o write-through?

En la mayoría de apps de producto: cache-aside falla menos porque puede degradar a MySQL cuando Redis es lento o está caído. Write-through hace que Redis forme parte de tu disponibilidad de escritura.

2) ¿Se puede hacer seguro el write-through?

Sí, pero “seguro” suele significar no una doble escritura verdaderamente síncrona. El modelo más seguro es commit en MySQL primero, luego actualización asíncrona de caché vía outbox/CDC con versionado.

3) ¿Por qué no confiar simplemente en la persistencia de Redis y omitir MySQL?

A veces eso es válido, pero es un diseño distinto. La persistencia y clusterización de Redis pueden funcionar, pero pierdes consultas relacionales y ganas nuevas restricciones operativas. No entres en eso dormido.

4) ¿Qué TTL debo usar?

Elige TTL según lo dolorosa que sea la obsolescencia y lo caro que sea un miss. Luego añade jitter (offset aleatorio) para evitar expiraciones sincronizadas. Las claves calientes a menudo necesitan manejo especial más allá del TTL.

5) ¿Debo actualizar la caché en la escritura o eliminar/invalidar?

Invalidar es más simple y a menudo más seguro, pero puede aumentar misses. Actualizar-en-escritura reduce misses pero aumenta la complejidad de corrección. Si actualizas-en-escritura, usa versionado o semántica CAS para evitar carreras.

6) ¿Cómo prevengo el stampede de caché?

Usa single-flight (lock por clave), servir obsoleto mientras se revalida, refresh anticipado y TTL con jitter. También considera negative caching para items inexistentes.

7) ¿Por qué mi hit rate es alto pero MySQL sigue caliente?

Porque los misses restantes pueden ser los más costosos, o porque las llamadas a Redis son lentas/grandes, o porque haces trabajo extra en MySQL por petición (joins, locks, queries secundarias) no relacionado con los objetos cacheados.

8) ¿Es necesario Redis Cluster para caching?

No. Muchas caches funcionan bien con primario+réplica y Sentinel para failover, o incluso una sola instancia si toleras pérdida. Cluster añade sobrecarga operativa y restricciones por slots de clave—vale la pena cuando necesitas escalado horizontal.

9) ¿Cómo depuro rápido quejas de “datos obsoletos”?

Elige un objeto, compara directamente valores entre Redis y MySQL y revisa lag de replicación. Luego identifica si es una carrera de invalidación (cache-aside) o una doble escritura parcial (write-through).

Conclusión: próximos pasos que puedes enviar

Si quieres algo que falle menos, elige cache-aside con timeouts ajustados, fallbacks sensatos y protección contra stampede. Trata a Redis como una capa de rendimiento, no como capa de verdad. Cuando Redis falle, deberías volverte más lento—no incorrecto.

Si realmente necesitas semántica write-through, no hagas dobles escrituras ingenuas en la vía de la petición. Haz que MySQL sea autoritativo, actualiza Redis después del commit y añade versionado para que escrituras viejas no puedan resucitar estado obsoleto.

Pasos concretos:

  1. Audita cada llamada a Redis: timeout, política de reintento y si la petición puede completarse sin Redis.
  2. Añade dashboards/alertas para latencia de Redis, evictions, blocked clients y lag de replicación de MySQL.
  3. Implementa TTL con jitter y single-flight en tus 20 claves con más QPS.
  4. Realiza un game day: deshabilita lecturas de Redis para un endpoint y verifica que MySQL pueda sobrevivir la carga el tiempo suficiente para una respuesta a incidentes.
  5. Escribe en palabras sencillas el límite de corrección y haz que se cumpla en las code reviews.

Los sistemas en producción no recompensan la astucia. Recompensan contratos que se mantienen bajo estrés.

← Anterior
Cómo leer reseñas de GPU: trampas de 1080p, 1440p y 4K
Siguiente →
MariaDB vs PostgreSQL en VPS: optimización para velocidad por dólar

Deja un comentario