Cómo usar listas de Redis (LPUSH, RPOP) como colas de mensajes

Aprende a transformar listas de Redis en un potente sistema de colas de mensajes. Este tutorial cubre los comandos esenciales LPUSH y RPOP, demostrando cómo encolar tareas y permitir que los trabajadores las desencolen y procesen de forma fiable. Explora ejemplos prácticos en Python y descubre consideraciones clave para construir colas de mensajes FIFO robustas con Redis para el procesamiento asíncrono de tareas.

Cómo usar listas de Redis (LPUSH, RPOP) como colas de mensajes

Las listas de Redis pueden formar una cola de mensajes pequeña y útil cuando necesitas algo más ligero que RabbitMQ, Kafka o un framework completo de trabajos en segundo plano. El patrón habitual es simple: los productores añaden trabajos con LPUSH, y los trabajadores toman trabajos con RPOP o BRPOP.

Esa simplicidad es la razón por la que la gente recurre a ello. Una solicitud web puede dejar un trabajo de correo electrónico en Redis y regresar rápidamente. Un trabajador puede recoger ese trabajo un momento después. No necesitas una topología de broker, intercambios, temas o una nueva pila operativa. Sin embargo, debes ser honesto sobre la compensación: RPOP simple elimina el mensaje antes de que el trabajador haya terminado el trabajo. Si el trabajador falla en el momento equivocado, ese trabajo se pierde a menos que construyas un patrón de reconocimiento alrededor de él.

Entendiendo las listas de Redis como colas

Una lista de Redis es una colección ordenada de cadenas. Puede verse como una secuencia de elementos, y Redis proporciona comandos para añadir o eliminar elementos desde el principio o el final de la lista. Esta naturaleza de doble extremo hace que las listas sean inherentemente adecuadas para implementaciones de colas.

  • Encolar (Añadir mensajes): Podemos añadir nuevos mensajes a la cola empujándolos hacia un extremo de la lista. El comando LPUSH empuja elementos al principio (lado izquierdo) de una lista.
  • Desencolar (Procesar mensajes): Podemos recuperar y eliminar mensajes de la cola sacándolos del otro extremo de la lista. El comando RPOP saca elementos del final (lado derecho) de una lista.

Esta combinación específica (LPUSH para encolar y RPOP para desencolar) crea una cola Primero en Entrar, Primero en Salir (FIFO), que es el comportamiento más común y esperado para una cola de mensajes.

Comandos principales: LPUSH y RPOP

Profundicemos en los dos comandos principales que forman la base de nuestra cola de mensajes de Redis.

LPUSH key value [value ...]

El comando LPUSH inserta uno o más valores de cadena al principio (lado izquierdo) de la lista almacenada en key. Si la key no existe, se crea una nueva lista y se insertan los valores.

Ejemplo:

Imagina que tienes una tarea que necesita ser procesada, como enviar un correo electrónico. Puedes empujar esta tarea como un mensaje a una lista de Redis llamada email_tasks.

# Empujar una sola tarea de correo electrónico
LPUSH email_tasks "{'to': '[email protected]', 'subject': 'Welcome!', 'body': 'Thanks for signing up!'}"

# Empujar otra tarea, se colocará antes de la anterior
LPUSH email_tasks "{'to': '[email protected]', 'subject': 'New User Registration', 'body': 'A new user has registered.'}"

Después de estos comandos, la lista email_tasks se verá así (de principio a fin):

1) "{'to': '[email protected]', 'subject': 'New User Registration', 'body': 'A new user has registered.'}"
2) "{'to': '[email protected]', 'subject': 'Welcome!', 'body': 'Thanks for signing up!'}"

RPOP key

El comando RPOP elimina y devuelve el último elemento (del final, lado derecho) de la lista almacenada en key. Si la lista está vacía, devuelve nil.

Ejemplo:

Un proceso trabajador puede sondear periódicamente la lista email_tasks en busca de nuevas tareas usando RPOP.

# Un trabajador intenta recuperar una tarea
RPOP email_tasks

Si la lista no está vacía, RPOP devolverá el último elemento empujado (que es el primer elemento desde el final). En nuestro ejemplo anterior, la primera llamada a RPOP devolvería:

"{'to': '[email protected]', 'subject': 'Welcome!', 'body': 'Thanks for signing up!'}"

Las llamadas subsiguientes recuperarían entonces la siguiente tarea disponible desde el final.

Construyendo un sistema básico de cola de mensajes

Esquematicemos el flujo típico de una cola de mensajes simple usando LPUSH y RPOP.

1. Productor (Encolado de tareas)

Cualquier parte de tu aplicación que necesite descargar trabajo puede actuar como productor. Construye un mensaje (a menudo una cadena JSON que representa los detalles de la tarea) y lo empuja a una lista de Redis usando LPUSH.

Lógica del productor (Ejemplo conceptual en Python):

import redis
import json

r = redis.Redis(host='localhost', port=6379, db=0)

def send_email_task(to_email, subject, body):
    task_message = {
        'type': 'send_email',
        'payload': {
            'to': to_email,
            'subject': subject,
            'body': body
        }
    }
    # LPUSH añade al principio de la lista 'email_queue'
    r.lpush('email_queue', json.dumps(task_message))
    print(f"Tarea de correo electrónico empujada a la cola: {to_email}")

# Ejemplo de uso:
send_email_task('[email protected]', 'Hola desde el Productor', 'Este es un mensaje de prueba.')
send_email_task('[email protected]', 'Actualización importante', 'Nuevas funciones disponibles.')

2. Consumidor (Desencolado y procesamiento de tareas)

Los procesos trabajadores, ejecutándose de forma independiente, monitorearán continuamente la lista de Redis en busca de nuevos mensajes. Usan RPOP para obtener y eliminar un mensaje de la cola.

Lógica del consumidor (Ejemplo conceptual en Python):

import redis
import json
import time

r = redis.Redis(host='localhost', port=6379, db=0)

def process_tasks():
    while True:
        # RPOP intenta obtener un mensaje del final de la lista 'email_queue'
        message_bytes = r.rpop('email_queue')
        if message_bytes:
            message_str = message_bytes.decode('utf-8')
            try:
                task = json.loads(message_str)
                print(f"Procesando tarea: {task}")
                # Simular procesamiento de tarea
                if task.get('type') == 'send_email':
                    print(f"  -> Enviando correo electrónico a {task['payload']['to']}...")
                    # Reemplazar con lógica real de envío de correo
                    time.sleep(1) # Simular trabajo
                    print(f"  -> Correo electrónico enviado a {task['payload']['to']}.")
                else:
                    print(f"  -> Tipo de tarea desconocido: {task.get('type')}")
            except json.JSONDecodeError:
                print(f"Error decodificando JSON: {message_str}")
            except Exception as e:
                print(f"Error procesando tarea {message_str}: {e}")
        else:
            # No hay mensaje, esperar un poco antes de sondear de nuevo
            # print("No hay tareas disponibles, esperando...")
            time.sleep(0.5)

if __name__ == "__main__":
    print("Trabajador iniciado. Esperando tareas...")
    process_tasks()

Cuando ejecutas el productor, empuja mensajes. Cuando ejecutas el consumidor, comenzará a recogerlos y procesarlos. El orden de procesamiento corresponderá al orden en que fueron empujados (FIFO) porque LPUSH añade al principio y RPOP elimina del final.

Consideraciones de fiabilidad

Mientras que LPUSH y RPOP proporcionan un mecanismo básico de cola, construir una cola de producción significa decidir qué debería suceder cuando los trabajadores fallan, los trabajos fallan o los productores envían cargas útiles malformadas.

1. Pérdida de mensajes durante el procesamiento

Si un proceso trabajador falla después de que RPOP haya eliminado el mensaje pero antes de que haya terminado de procesarlo, ese mensaje se pierde. Para prevenir esto:

  • Usa BRPOP en lugar de sondeo intensivo: BRPOP bloquea hasta que una lista tenga un elemento o hasta que se produzca un tiempo de espera. Esto no hace que el procesamiento sea fiable por sí mismo, pero evita que los trabajadores se despierten cada pocos milisegundos solo para encontrar una cola vacía.
    # Pop bloqueante desde la derecha, con un tiempo de espera de 0 (bloquear indefinidamente)
    BRPOP email_queue 0
    
  • Usa una lista de procesamiento para el reconocimiento: Un patrón común es mover atómicamente un mensaje de email_queue a email_processing, procesarlo y luego eliminarlo de email_processing solo después de que el trabajo tenga éxito. Si un trabajador muere, un proceso segador separado puede buscar elementos obsoletos en la lista de procesamiento y moverlos de vuelta a la cola principal. RPOPLPUSH es el comando clásico para este patrón, y las versiones más nuevas de Redis también proporcionan LMOVE/BLMOVE.

2. Manejo de tareas fallidas

¿Qué sucede si una tarea falla durante el procesamiento (por ejemplo, debido a un problema de red temporal o datos incorrectos)?

  • Mecanismos de reintento: Implementa lógica de reintento dentro del trabajador. Después de algunos fallos, mueve la tarea a una lista 'failed_tasks' para inspección manual.
  • Cola de mensajes fallidos (DLQ): Una lista de Redis dedicada (u otro almacenamiento) donde se envían los mensajes que fallan repetidamente en el procesamiento. Esto es crucial para la depuración y la recuperación.

3. Múltiples consumidores

Si tienes múltiples instancias de trabajadores consumiendo de la misma cola, RPOP (y BRPOP) asegura que cada mensaje sea procesado por solo un trabajador. Esto se debe a que RPOP elimina atómicamente el elemento.

4. Orden de los mensajes

Mientras que LPUSH y RPOP crean una cola FIFO, esta garantía solo es tan fuerte como tu lógica de procesamiento. Si los consumidores reencolan mensajes fallidos sin un manejo adecuado, o si introduces otras operaciones, el estricto orden FIFO podría verse comprometido.

5. Formato de la carga útil e idempotencia

Trata el cuerpo del mensaje como un pequeño contrato. JSON es común porque es fácil de inspeccionar en redis-cli, pero usa JSON válido en lugar de diccionarios con comillas simples al estilo de Python:

{"type":"send_email","id":"email-1842","payload":{"to":"[email protected]","template":"welcome"}}

El campo id importa. Si un trabajador reintenta después de un tiempo de espera, o si un trabajo obsoleto se vuelve a encolar desde una lista de procesamiento, el mismo trabajo lógico puede ejecutarse más de una vez. Diseña el manejador para que un duplicado sea inofensivo. Para un trabajador de correo electrónico, eso podría significar registrar email-1842 en la base de datos de la aplicación antes de enviar, luego verificar ese registro antes de que cualquier reintento envíe otro mensaje.

6. Longitud de la cola y contrapresión

Vigila la longitud de la cola con LLEN email_queue. Una cola en crecimiento no es automáticamente mala; puede simplemente significar que los trabajadores se están poniendo al día después de un pico de tráfico. Una cola que crece durante horas generalmente significa que los productores son más rápidos que los consumidores, los trabajadores están fallando o una dependencia lenta lo está reteniendo todo.

En la práctica, me gusta alertar también sobre la antigüedad además de la longitud. Las listas de Redis no almacenan el tiempo de encolado por separado, así que pon una marca de tiempo en la carga útil si la antigüedad del trabajo importa:

{"type":"resize_image","id":"img-991","created_at":"2026-05-24T08:15:00Z","payload":{"image_id":991}}

Entonces los registros de tu trabajador pueden decirte si los trabajos se están procesando con segundos de retraso o con horas de retraso. Eso es mucho más útil que la longitud sola cuando estás depurando un incidente real.

Técnicas avanzadas (Brevemente)

  • RPOPLPUSH: Saca atómicamente un mensaje de una lista y lo empuja a otra (por ejemplo, una lista de 'procesamiento'). Este es un comando clave para implementar procesamiento fiable con reconocimientos.
  • BLPOP / BRPOP con múltiples claves: Bloquea y saca de la primera lista que se vuelva no vacía. Útil para consumir de múltiples colas.
  • Scripting Lua: Para operaciones atómicas complejas que RPOPLPUSH no cubre, se pueden usar scripts Lua para asegurar que secuencias críticas de comandos se ejecuten sin interrupción.

Una forma de trabajador más fiable

Para cualquier cosa más importante que una notificación de mejor esfuerzo, evita el trabajador simple de "sacar y esperar". Una forma más segura se ve así:

RPOPLPUSH email_queue email_processing

El trabajador recibe el mensaje y Redis ya lo ha movido a email_processing. Después de que el correo electrónico se envía y la aplicación registra el éxito, el trabajador elimina esa carga útil exacta de la lista de procesamiento:

LREM email_processing 1 '{"type":"send_email","id":"email-1842"}'

Eso todavía no es una cola empresarial perfecta. LREM debe coincidir con la carga útil, las listas de procesamiento grandes pueden volverse incómodas y necesitas un proceso segador que sepa cuándo un mensaje es lo suficientemente antiguo para reintentar. Pero cambia el modo de fallo de una manera útil. Un fallo del trabajador ya no elimina la única copia del trabajo.

Si usas este enfoque, pon metadatos de reintento en el mensaje o almacénalos junto al ID del mensaje en otra clave. Por ejemplo, un segador puede mover un mensaje obsoleto de vuelta a email_queue las primeras veces, luego moverlo a email_failed después del límite de reintentos. Eso te da un lugar para inspeccionar mensajes venenosos en lugar de ver la misma carga útil incorrecta fallar para siempre.

Cuándo las listas de Redis son la cola incorrecta

Las listas de Redis son fáciles de entender, pero no siempre son la herramienta adecuada. Si necesitas trabajos retrasados, prioridades de trabajos, reintentos programados, visibilidad del flujo de trabajo o un historial de auditoría a largo plazo, una biblioteca de trabajos o un broker dedicado pueden ser menos trabajo al final. Los flujos (Streams) de Redis también merecen consideración porque tienen grupos de consumidores y semántica de reconocimiento integrados en el tipo de dato.

Todavía me gustan las listas para colas internas pequeñas: generación de miniaturas, calentamiento de caché, distribución de webhooks donde el sistema fuente puede reintentar, o trabajos simples en segundo plano propiedad de una sola aplicación. En el momento en que múltiples equipos dependen del contrato de la cola, anota las expectativas de entrega. "Al menos una vez", "como máximo una vez" y "mejor esfuerzo" no son términos académicos durante un incidente. Deciden si los duplicados son aceptables, si los mensajes perdidos son tolerables y cuánta maquinaria de recuperación necesitas.

Las listas de Redis son una buena opción cuando necesitas una cola pequeña y comprensible y ya operas Redis. Comienza con LPUSH y BRPOP para trabajo simple en segundo plano. Añade una lista de procesamiento, un contador de reintentos y una lista de mensajes fallidos una vez que perder un trabajo importe. Si necesitas programación retrasada, prioridades, distribución, retención prolongada o fuertes garantías de entrega a través de muchos servicios, ese suele ser el punto en el que una cola construida para un propósito se vuelve más fácil de manejar que una pila creciente de convenciones de Redis.