Implementar el patron Cache-Aside con Redis
Usa el patron cache-aside para leer y escribir datos a traves de Redis, manejando cache misses, lecturas obsoletas e invalidacion write-through
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Implementar el patron Cache-Aside con Redis
Cache-aside (tambien llamado lazy loading) es el patron de caching mas comun. La aplicacion verifica la cache primero; en un miss, carga desde la base de datos, escribe el resultado en la cache y lo retorna. En escrituras, la aplicacion actualiza la base de datos e invalida la entrada de cache. A continuacion se implementa cache-aside en Python con Redis, incluyendo proteccion contra thundering herd e invalidacion write-through.
Cuando Usar Esto
-
For alternatives, see Complete Guide to Redis Caching Strategies.
-
Workloads de lectura intensiva donde los mismos datos se acceden repetidamente
-
Datos que cambian infrecuentemente pero son costosos de obtener
-
Quieres que la cache sea opcional — el sistema funciona sin ella
Requisitos Previos
- Python 3.10+
- Paquete
redis(pip install redis) - Un cliente de base de datos (SQLAlchemy, Psycopg o similar)
Solucion
1. Instalar dependencias
pip install redis
2. Implementar lectura cache-aside
import json
import logging
from redis import Redis
logger = logging.getLogger(__name__)
class CacheAside:
def __init__(self, redis_client: Redis, default_ttl: int = 300):
self.redis = redis_client
self.default_ttl = default_ttl
def get_or_load(
self,
key: str,
loader: callable,
ttl: int | None = None,
) -> dict | None:
"""Read from cache, or load from source and cache the result.
Args:
key: Cache key.
loader: Function that loads data from the source on cache miss.
ttl: Override default TTL in seconds.
Returns:
Cached or freshly loaded data.
"""
try:
cached = self.redis.get(key)
if cached is not None:
return json.loads(cached)
except Exception as e:
logger.warning("Cache read failed for %s: %s", key, e)
result = loader()
if result is None:
return None
try:
self.redis.setex(key, ttl or self.default_ttl, json.dumps(result, default=str))
except Exception as e:
logger.warning("Cache write failed for %s: %s", key, e)
return result
def invalidate(self, key: str) -> None:
"""Remove a key from the cache."""
try:
self.redis.delete(key)
except Exception as e:
logger.warning("Cache invalidation failed for %s: %s", key, e)
def invalidate_pattern(self, pattern: str) -> int:
"""Delete all keys matching a pattern (e.g., 'user:*')."""
count = 0
try:
for key in self.redis.scan_iter(match=pattern, count=100):
self.redis.delete(key)
count += 1
except Exception as e:
logger.warning("Pattern invalidation failed for %s: %s", pattern, e)
return count
3. Implementar invalidacion write-through
from myapp.database import db
cache = CacheAside(redis_client, default_ttl=300)
def get_user(user_id: str) -> dict | None:
return cache.get_or_load(
key=f"user:{user_id}",
loader=lambda: db.users.find_by_id(user_id),
ttl=120,
)
def update_user(user_id: str, data: dict) -> dict:
user = db.users.update(user_id, data)
cache.invalidate(f"user:{user_id}")
return user
def delete_user(user_id: str) -> None:
db.users.delete(user_id)
cache.invalidate(f"user:{user_id}")
cache.invalidate_pattern(f"user_posts:{user_id}:*")
4. Proteccion contra thundering herd
Cuando una entrada de cache expira, multiples peticiones pueden disparar el loader simultaneamente, causando un pico en la base de datos. Un lock previene esto:
import time
import uuid
class CacheAsideWithLock(CacheAside):
def get_or_load(
self,
key: str,
loader: callable,
ttl: int | None = None,
lock_timeout: int = 10,
) -> dict | None:
# Intentar cache primero
try:
cached = self.redis.get(key)
if cached is not None:
return json.loads(cached)
except Exception:
pass
# Adquirir lock para prevenir thundering herd
lock_key = f"lock:{key}"
lock_token = str(uuid.uuid4())
acquired = self.redis.set(lock_key, lock_token, nx=True, ex=lock_timeout)
if acquired:
try:
result = loader()
if result is not None:
self.redis.setex(key, ttl or self.default_ttl, json.dumps(result, default=str))
return result
finally:
# Liberar lock solo si todavia lo poseemos
self._release_lock(lock_key, lock_token)
else:
# Esperar y reintentar
for _ in range(5):
time.sleep(0.1)
cached = self.redis.get(key)
if cached is not None:
return json.loads(cached)
# Lock expiro — cargar de todas formas como fallback
return loader()
def _release_lock(self, lock_key: str, token: str) -> None:
"""Release lock using Lua script to prevent releasing someone else's lock."""
script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
self.redis.eval(script, 1, lock_key, token)
5. Calentamiento de cache
Pre-poblar la cache antes de que llegue el trafico:
def warm_user_cache(user_ids: list[str]) -> None:
users = db.users.find_many({"id": {"in": user_ids}})
for user in users:
cache_key = f"user:{user['id']}"
redis_client.setex(cache_key, 300, json.dumps(user, default=str))
logger.info("Warmed cache for %d users", len(users))
Como Funciona
- Lectura de cache —
get_or_loadverifica Redis primero. En un hit, retorna el valor cacheado sin tocar la base de datos. - Cache miss — la funcion
loaderobtiene desde la base de datos. El resultado se escribe a Redis con un TTL para que auto-expire. - Invalidacion write-through — despues de cualquier escritura a la base de datos,
invalidateelimina la entrada de cache para que la siguiente lectura cargue datos frescos. - Lock de thundering herd — en un cache miss, solo una peticion adquiere el lock y carga desde la base de datos. Las demas esperan y reintentan la lectura de cache.
- Invalidacion por patron —
invalidate_patternusaSCANpara encontrar y eliminar claves que coinciden con un patron glob, util para invalidar todas las entradas de un usuario.
Variantes
Read-Through con Refresh-Ahead
Refresca la cache antes de que expire:
def get_or_refresh(key: str, loader: callable, ttl: int = 300) -> dict | None:
cached = redis_client.get(key)
if cached:
result = json.loads(cached)
# Verificar si la entrada esta cerca de expirar (dentro del 10% del TTL)
ttl_remaining = redis_client.ttl(key)
if ttl_remaining < ttl * 0.1:
# Refrescar en background
threading.Thread(target=lambda: cache.get_or_load(key, loader, ttl), daemon=True).start()
return result
return cache.get_or_load(key, loader, ttl)
Write-Behind (Write-Back)
Escribe en la cache primero, luego persiste asincronamente a la base de datos:
def write_behind_update(user_id: str, data: dict) -> dict:
cache_key = f"user:{user_id}"
redis_client.setex(cache_key, 300, json.dumps(data, default=str))
redis_client.lpush("write_queue:user", json.dumps({"id": user_id, "data": data}))
return data
# Worker en background procesa la cola
def process_write_queue():
while True:
item = redis_client.brpop("write_queue:user", timeout=10)
if item:
payload = json.loads(item[1])
db.users.update(payload["id"], payload["data"])
Mejores Practicas
- Establece un TTL en cada entrada de cache — previene que datos obsoletos persistan indefinidamente
- Invalida al escribir — actualiza la base de datos primero, luego elimina la entrada de cache (no al reves)
- Maneja fallos de Redis elegantemente — la cache es una optimizacion; el loader debe seguir funcionando sin ella
- Usa TTLs cortos para datos mutables — 30-120 segundos para datos de usuario, mas largo para datos de referencia
Errores Comunes
- Actualizar la cache antes que la base de datos — si la escritura a la base de datos falla, la cache tiene datos obsoletos
- No manejar cache misses — si el loader retorna
None, no lo caches (o cachealo con un TTL corto para prevenir misses repetidos) - Usar
KEYSen lugar deSCAN—KEYSbloquea Redis;SCANes non-blocking y seguro para produccion - Cachear demasiado — cachea solo datos calientes; datos frios desperdician memoria y aumentan la overhead de invalidacion
FAQ
Q: Cache-aside vs. read-through — cual es la diferencia? A: En cache-aside, la aplicacion gestiona lecturas y escrituras de cache. En read-through, una libreria de cache lo maneja transparentemente. Cache-aside da mas control; read-through es mas simple.
Q: Debo actualizar la cache o invalidarla al escribir? A: Invalida (elimina). La siguiente lectura cargara datos frescos. Actualizar la cache arriesga inconsistencia si la escritura a la base de datos falla.
Q: Como manejo cache stampede? A: Usa el patron de lock mostrado arriba, o coalescing de peticiones — solo una peticion carga datos mientras las demas esperan.
Q: Que TTL debo usar? A: Empieza con 300 segundos y ajusta. Monitorea el hit rate de cache — por debajo del 80% significa que el TTL es demasiado corto o los datos cambian muy rapido.
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Recursos Relacionados
Caching with Redis
How to implement application caching using Redis for performance and scalability.
RecipeCache Function Results with Redis and TTL in Python
Build a Python decorator that caches function return values in Redis with configurable TTL, key generation, and cache invalidation
PatternCache-Aside Pattern
Load data into the cache on demand from the backing store. A caching pattern that gives the application full control over what and when to cache.
RecipeCDN Cache Invalidation Strategies and Patterns
Implement CDN cache invalidation using purge APIs, surrogate keys, tag-based invalidation, and versioned URLs to keep content fresh
RecipeHTTP Cache-Control Headers for APIs and Static Assets
Set Cache-Control, ETag, and Last-Modified headers to control browser and CDN caching for API responses and static assets
RecipeMulti-Level Cache with In-Memory L1 and Redis L2
Implement a two-level cache combining in-memory L1 and Redis L2 for low-latency reads with cross-instance consistency
RecipeDistributed Locking with Redis and Redlock
Implement distributed locks with Redis for mutual exclusion across processes, using SET NX with TTL and the Redlock algorithm for reliability