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.
Redis Pub/Sub para mensajeria entre procesos
Redis pub/sub permite que procesos se comuniquen a traves de canales sin acoplamiento directo. Un publicador envia un mensaje a un canal; todos los procesos suscritos lo reciben. Esto es util para invalidacion de cache entre instancias, notificaciones en tiempo real y arquitecturas event-driven donde los servicios necesitan reaccionar a cambios sin polling.
Cuando Usar Esto
-
For alternatives, see Implement Redis Pub/Sub Messaging in Python.
-
Invalidacion de cache entre multiples instancias de servidor
-
Notificaciones en tiempo real (usuario conectado, nuevo mensaje, cambio de estado)
-
Microservicios event-driven que reaccionan a eventos de dominio
-
Desacoplar productores de consumidores sin una cola de mensajes
Requisitos Previos
- Python 3.10+
- Paquete
redis(pip install redis) - Una instancia de Redis corriendo
Solucion
1. Instalar dependencias
pip install redis
2. Publicador — difundir eventos
import json
import logging
import time
from redis import Redis
logger = logging.getLogger(__name__)
class EventPublisher:
def __init__(self, redis_client: Redis):
self.redis = redis_client
def publish(self, channel: str, event: dict) -> int:
"""Publish an event to a channel.
Args:
channel: Channel name (e.g., 'user.events').
event: Event payload as a dict.
Returns:
Number of subscribers that received the message.
"""
message = json.dumps(event, default=str)
receivers = self.redis.publish(channel, message)
logger.info("Published to %s: %d receivers", channel, receivers)
return receivers
def publish_user_event(self, event_type: str, user_id: str, data: dict) -> int:
return self.publish("user.events", {
"type": event_type,
"userId": user_id,
"data": data,
"timestamp": int(time.time()),
})
3. Suscriptor — escuchar eventos
import json
import threading
from redis import Redis
class EventSubscriber:
def __init__(self, redis_client: Redis):
self.redis = redis_client
self._threads: list[threading.Thread] = []
def subscribe(
self,
channels: list[str],
handler: callable,
) -> None:
"""Subscribe to channels and process messages with a handler.
Args:
channels: List of channel names to subscribe to.
handler: Function called with (channel, event_dict) for each message.
"""
pubsub = self.redis.pubsub()
pubsub.subscribe(*channels)
thread = threading.Thread(
target=self._listen,
args=(pubsub, handler),
daemon=True,
)
thread.start()
self._threads.append(thread)
def _listen(self, pubsub, handler: callable) -> None:
for message in pubsub.listen():
if message["type"] == "message":
try:
event = json.loads(message["data"])
handler(message["channel"], event)
except json.JSONDecodeError as e:
logging.warning("Invalid message on %s: %s", message["channel"], e)
except Exception as e:
logging.error("Handler error on %s: %s", message["channel"], e)
def stop_all(self) -> None:
for thread in self._threads:
thread.join(timeout=5)
4. Suscripcion por patron
Suscribirse a canales que coinciden con un patron glob:
def subscribe_pattern(
self,
pattern: str,
handler: callable,
) -> None:
"""Subscribe to channels matching a glob pattern."""
pubsub = self.redis.pubsub()
pubsub.psubscribe(pattern)
thread = threading.Thread(
target=self._listen_pattern,
args=(pubsub, handler),
daemon=True,
)
thread.start()
self._threads.append(thread)
def _listen_pattern(self, pubsub, handler: callable) -> None:
for message in pubsub.listen():
if message["type"] == "pmessage":
try:
event = json.loads(message["data"])
handler(message["channel"], event)
except Exception as e:
logging.error("Pattern handler error: %s", e)
5. Caso de uso — invalidacion de cache entre instancias
# Instancia 1 — publica invalidacion al actualizar
publisher = EventPublisher(redis_client)
def update_product(product_id: str, data: dict) -> dict:
product = db.products.update(product_id, data)
cache.delete(f"product:{product_id}")
publisher.publish("cache.invalidate", {
"action": "delete",
"key": f"product:{product_id}",
})
return product
# Instancia 2 — se suscribe e invalida su cache local
subscriber = EventSubscriber(redis_client)
def handle_invalidation(channel: str, event: dict):
if event["action"] == "delete":
local_cache.delete(event["key"])
elif event["action"] == "clear_pattern":
local_cache.clear_pattern(event["pattern"])
subscriber.subscribe(["cache.invalidate"], handle_invalidation)
6. Caso de uso — notificaciones en tiempo real
# Servidor WebSocket — se suscribe a canales de notificacion de usuario
subscriber = EventSubscriber(redis_client)
def handle_notification(channel: str, event: dict):
user_id = event["userId"]
websocket_manager.send_to_user(user_id, event)
# Suscribirse a todos los canales de notificacion
subscriber.subscribe_pattern("notifications:*", handle_notification)
# Servidor API — publica notificaciones
publisher = EventPublisher(redis_client)
def notify_user(user_id: str, message: str):
publisher.publish(f"notifications:{user_id}", {
"type": "notification",
"userId": user_id,
"message": message,
})
Como Funciona
PUBLISHenvia un mensaje a todos los suscriptores de un canal. Redis retorna el numero de receptores — si es cero, nadie esta escuchando.SUBSCRIBEbloquea y escucha mensajes en los canales especificados. Cada mensaje incluye el nombre del canal y los datos.PSUBSCRIBEse suscribe a canales que coinciden con un patron glob (ej.notifications:*), util para enrutar a canales dinamicos.- Threads se usan porque
pubsub.listen()es un iterador bloqueante. Cada suscripcion corre en un thread daemon para que no impida la salida del proceso. - Los mensajes son efimeros — si no hay suscriptor escuchando, el mensaje se pierde. Usa Redis Streams para mensajeria persistente.
Variantes
Redis Streams para mensajeria persistente
Cuando los mensajes no deben perderse, usa Streams en lugar de pub/sub:
# Productor
redis_client.xadd("events:users", {
"type": "created",
"userId": "123",
"data": json.dumps(user_data),
})
# Consumidor con consumer group
def consume_events():
while True:
messages = redis_client.xreadgroup(
"event-workers",
"worker-1",
{"events:users": ">"},
count=10,
block=5000,
)
for stream, msg_list in messages:
for msg_id, fields in msg_list:
process_event(fields)
redis_client.xack(stream, "event-workers", msg_id)
Sharded Pub/Sub (Redis 7.0+)
Para despliegues a gran escala, usa sharded pub/sub para distribuir el trafico de canales entre shards del cluster:
# Publicador
redis_client.spublish("user.events", message)
# Suscriptor
pubsub = redis_client.ssubscribe("user.events")
for message in pubsub.listen():
if message["type"] == "smessage":
handler(message["channel"], json.loads(message["data"]))
Apagado elegante
import signal
def setup_graceful_shutdown(subscriber: EventSubscriber):
def shutdown(signum, frame):
logging.info("Shutting down subscriber...")
subscriber.stop_all()
sys.exit(0)
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)
Mejores Practicas
- Usa pub/sub para eventos fire-and-forget — los mensajes no se persisten; si la entrega importa, usa Streams
- Serializa con JSON — mantén los mensajes pequenos y legibles; usa MessagePack para alto throughput
- Maneja errores en el listener — una excepcion no manejada en el handler mata el thread del listener
- Usa threads daemon — previene que el suscriptor mantenga vivo el proceso al apagar
Errores Comunes
- Esperar garantias de entrega de mensajes — pub/sub no persiste mensajes; los suscriptores offline los pierden
- Suscribir con la misma conexion Redis usada para comandos — Redis requiere una conexion dedicada para suscripciones
- No manejar reconexion — si la conexion Redis cae, el suscriptor deja de recibir mensajes silenciosamente
- Publicar payloads grandes — Redis procesa mensajes en el thread principal; payloads grandes bloquean todas las operaciones
FAQ
Q: Que pasa si nadie esta suscrito a un canal?
A: El mensaje se descarta. PUBLISH retorna 0. Esto es por diseno — pub/sub es fire-and-forget.
Q: Puedo usar pub/sub con Redis Cluster?
A: Si, pero los mensajes se difunden a todos los nodos. Usa sharded pub/sub (SPUBLISH/SSUBSCRIBE) en Redis 7.0+ para mejor escalabilidad.
Q: Debo usar pub/sub o Redis Streams? A: Usa pub/sub para notificaciones en tiempo real donde la perdida de mensajes es aceptable. Usa Streams cuando necesitas persistencia, consumer groups y entrega at-least-once.
Q: Cuantos suscriptores puede tener un canal? A: Miles. Redis maneja el fan-out eficientemente, pero cada suscriptor anade memoria para el buffer de salida.
Q: ¿Qué pasa con los mensajes cuando no hay suscriptores escuchando? A: Los mensajes se descartan. Redis Pub/Sub no persiste mensajes. Si la durabilidad importa, usa Redis Streams en su lugar — almacenan mensajes y permiten que los consumer groups lean a su propio ritmo.
Q: ¿Puedo usar suscripciones por patrón con sharded Pub/Sub?
A: No. Sharded Pub/Sub (SPUBLISH/SSUBSCRIBE) no soporta pattern matching con globs. Usa PUBLISH/PSUBSCRIBE regular para suscripciones por patrón, pero nota que estas no se benefician del sharding del cluster.
Errores Comunes
- Asumir que los mensajes se entregan de forma confiable — Pub/Sub descarta mensajes cuando los suscriptores están desconectados
- No manejar lógica de reconexión — los suscriptores deben resuscribirse después de una caída de conexión
- Usar Pub/Sub para colas de tareas — usa Redis Streams o Lists con
BRPOPen su lugar - Publicar payloads grandes (>1MB) — Redis bloquea el cliente que publica durante el fan-out
- No setear timeout en conexiones de suscriptores — los consumidores lentos pueden acumular backlog en el buffer de salida
- Usar Pub/Sub para notificaciones críticas sin una cola de fallback — si las garantías de entrega importan, combina con Redis Streams
¿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
Implement the Cache-Aside Pattern with Redis
Use the cache-aside pattern to read and write data through Redis, handling cache misses, stale reads, and write-through invalidation
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
PatternVoucher Pattern
Validate claims and delegate access using signed vouchers without exposing sensitive data. A security pattern for token-based authorization between services.
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
RecipeBuild a Real-Time Leaderboard with Redis Sorted Sets
Use Redis sorted sets to implement real-time leaderboards with rank tracking, score updates, and top-N queries in O(log N) time