Cache de resultados de funciones con Redis y TTL en Python
Construye un decorador de Python que cachea resultados de funciones en Redis con TTL configurable, generacion de claves e invalidacion de cache
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.
Cache de resultados de funciones con Redis y TTL en Python
Cachear resultados costosos de funciones en Redis reduce latencia y carga de base de datos. Un enfoque con decorador permite agregar caching a cualquier funcion con una sola anotacion @cached, manteniendo la logica de negocio limpia. Esta receta construye un decorador de cache respaldado por Redis con generacion automatica de claves, serializacion JSON, TTL configurable e invalidacion manual.
Cuando Usar Esto
-
For alternatives, see Complete Guide to Redis Caching Strategies.
-
Funciones que retornan el mismo resultado para los mismos argumentos (funciones puras o casi puras)
-
Consultas a base de datos, llamadas a API o computaciones costosas que raramente cambian
-
Resultados seguros de servir obsoletos durante la duracion del TTL
Requisitos Previos
- Python 3.10+
- Paquete
redis(pip install redis) - Una instancia de Redis corriendo
Solucion
1. Instalar dependencias
pip install redis
2. Construir el decorador de cache
import functools
import hashlib
import json
import logging
from typing import Any, Callable, TypeVar
from redis import Redis
logger = logging.getLogger(__name__)
T = TypeVar("T")
def cached(
redis_client: Redis,
ttl: int = 300,
prefix: str = "cache",
skip_args: tuple[str, ...] = (),
) -> Callable:
"""Cache function results in Redis with a TTL.
Args:
redis_client: Redis client instance.
ttl: Time-to-live in seconds.
prefix: Cache key prefix.
skip_args: Argument names to exclude from the cache key.
Returns:
Decorator that wraps the function with Redis caching.
"""
def decorator(func: Callable[..., T]) -> Callable[..., T]:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> T:
key = _make_key(func, args, kwargs, prefix, skip_args)
try:
cached_value = redis_client.get(key)
if cached_value is not None:
return json.loads(cached_value)
except (json.JSONDecodeError, ConnectionError) as e:
logger.warning("Cache read failed for %s: %s", key, e)
result = func(*args, **kwargs)
try:
redis_client.setex(key, ttl, json.dumps(result, default=str))
except (TypeError, ConnectionError) as e:
logger.warning("Cache write failed for %s: %s", key, e)
return result
wrapper.cache_invalidate = _make_invalidator(redis_client, func, prefix, skip_args) # type: ignore
return wrapper
return decorator
def _make_key(
func: Callable,
args: tuple,
kwargs: dict,
prefix: str,
skip_args: tuple[str, ...],
) -> str:
"""Generate a deterministic cache key from function name and arguments."""
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
filtered = {
k: v for k, v in bound.arguments.items()
if k not in skip_args and k != "self"
}
arg_hash = hashlib.sha256(
json.dumps(filtered, sort_keys=True, default=str).encode()
).hexdigest()[:16]
return f"{prefix}:{func.__module__}:{func.__name__}:{arg_hash}"
def _make_invalidator(
redis_client: Redis,
func: Callable,
prefix: str,
skip_args: tuple[str, ...],
) -> Callable:
"""Create a cache invalidation function for a specific decorated function."""
def invalidate(*args, **kwargs) -> int:
key = _make_key(func, args, kwargs, prefix, skip_args)
return redis_client.delete(key)
return invalidate
3. Usar el decorador
import redis
from myapp.database import get_user_by_id
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
@cached(r, ttl=60, prefix="user")
def get_user(user_id: str, include_posts: bool = False) -> dict:
user = get_user_by_id(user_id)
if include_posts:
user["posts"] = fetch_user_posts(user_id)
return user
# Primera llamada — toca la base de datos, cachea el resultado
user = get_user("123", include_posts=True)
# Segunda llamada — retorna desde cache
user = get_user("123", include_posts=True)
# Invalidar la cache para argumentos especificos
get_user.cache_invalidate("123", include_posts=True)
4. Version asincrona
import asyncio
from redis.asyncio import Redis as AsyncRedis
def async_cached(
redis_client: AsyncRedis,
ttl: int = 300,
prefix: str = "cache",
) -> Callable:
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(*args, **kwargs):
key = _make_key(func, args, kwargs, prefix, ())
try:
cached = await redis_client.get(key)
if cached is not None:
return json.loads(cached)
except Exception as e:
logger.warning("Async cache read failed: %s", e)
result = await func(*args, **kwargs)
try:
await redis_client.setex(key, ttl, json.dumps(result, default=str))
except Exception as e:
logger.warning("Async cache write failed: %s", e)
return result
return wrapper
return decorator
# Uso
r = AsyncRedis(host="localhost", port=6379, decode_responses=True)
@async_cached(r, ttl=120, prefix="api")
async def fetch_weather(city: str) -> dict:
async with httpx.AsyncClient() as client:
resp = await client.get(f"https://api.weather.example/{city}")
return resp.json()
Como Funciona
- Generacion de claves usa el modulo y nombre de la funcion mas un hash SHA-256 de sus argumentos para crear una clave determinista y resistente a colisiones.
skip_argsexcluye argumentos comoselfu objetos de peticion de la clave, para que los metodos de instancia funcionen correctamente.setexestablece el valor con expiracion en un comando atomico de Redis, para que la entrada expire automaticamente despues del TTL.cache_invalidatese adjunta a la funcion wrapper, permitiendo a los llamadores eliminar una entrada especifica pasando los mismos argumentos.- Degradacion elegante — si Redis cae, la funcion sigue ejecutandose; el decorador registra el error y retorna el resultado sin cache.
Variantes
Cache-Aside con refresh manual
@cached(r, ttl=300, prefix="product")
def get_product(product_id: str) -> dict:
return db.products.find_by_id(product_id)
def refresh_product(product_id: str) -> dict:
get_product.cache_invalidate(product_id)
return get_product(product_id)
Invalidacion basada en tags
Invalida todas las entradas de cache para un tag (ej. todas las caches relacionadas con usuarios):
def invalidate_tag(redis_client: Redis, tag: str) -> int:
keys = redis_client.smembers(f"tag:{tag}")
if keys:
redis_client.delete(*keys)
redis_client.delete(f"tag:{tag}")
return len(keys)
# Al cachear, agrega la clave a un set de tag
redis_client.sadd("tag:users", key)
Compresion para valores grandes
import zlib
@cached(r, ttl=600, prefix="report")
def generate_report(date: str) -> dict:
data = heavy_computation(date)
return data # Podria ser grande
# En el decorador, comprimir antes de almacenar:
redis_client.setex(key, ttl, zlib.compress(json.dumps(result).encode()))
Mejores Practicas
- Usa TTLs cortos para datos que cambian frecuentemente — 30-60 segundos para feeds de usuario, 5-10 minutos para datos de referencia
- Cachea solo resultados serializables — el decorador usa
json.dumps; objetos con tipos personalizados necesitan un serializadordefault - Maneja fallos de Redis elegantemente — la cache es una optimizacion, no la fuente de verdad; la funcion debe seguir funcionando sin ella
- Invalida al escribir — llama
cache_invalidatedespues de cualquier mutacion que cambiaria el resultado cacheado
Errores Comunes
- Cachear funciones con efectos secundarios — si la funcion escribe a la base de datos, cachear omite la escritura en cache hits
- Usar argumentos por defecto mutables — el hash de la clave cambia si una lista o dict por defecto se modifica entre llamadas
- No establecer un TTL — sin
setex, las entradas persisten indefinidamente y consumen memoria de Redis - Incluir
selfen la clave — elid()del objeto cambia entre peticiones, causando cache misses
FAQ
Q: Debo usar Redis o cache en memoria?
A: Usa Redis cuando multiples procesos o servidores necesitan compartir la cache. Usa memoria (functools.lru_cache) para caches de un solo proceso y corta duracion.
Q: Como manejo valores de retorno no serializables?
A: Pasa una funcion default a json.dumps que convierta tipos personalizados a dicts, o usa pickle (con precauciones de seguridad).
Q: Que TTL debo usar? A: Empieza con 300 segundos (5 minutos) y ajusta segun tolerancia a obsolescencia y frecuencia de escritura. TTLs mas cortos reducen obsolescencia pero aumentan cache misses.
Q: Puedo cachear funciones que retornan None?
A: Si, pero distingue entre “None cacheado” y “cache miss” verificando la existencia de la clave con redis_client.exists(key) en lugar de redis_client.get(key).
¿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.
RecipeRedis Cache Patterns for High-Performance Applications
How to implement cache-aside, write-through, and write-behind patterns with Redis to reduce database load and improve response times
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.
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
RecipeCache Database Query Results with Redis and Python
Cache expensive database query results in Redis with cache-aside pattern, TTL management, and invalidation on writes for Python applications.
RecipeConfigure Caffeine Cache in Java with Eviction Policies
Set up Caffeine cache in a Java application with size-based, time-based, and weighted eviction policies for high-performance local caching.
RecipeCache HTTP Responses with Nginx Reverse Proxy
Configure Nginx as a caching reverse proxy to cache upstream HTTP responses with TTL zones, cache keys, and conditional purging.