Volver al inicio

Cola Redis Lua: orden en particiones y pool de trabajadores

El artículo describe la implementación de una cola de tareas en Redis con scripts Lua para garantizar orden en particiones con un pool de trabajadores compartido. Soporte para mensajes diferidos, reintentos, benchmarks de rendimiento. Adecuado para servicios de alta carga sin Kafka.

Scripts Lua en Redis: cola con orden y retraso
Advertisement 728x90

Cola Redis con Lua: Orden Garantizado en Particiones con un Grupo Compartido de Trabajadores

En sistemas de alta carga, el procesamiento de mensajes requiere un orden estricto dentro de particiones lógicas a través de cientos de miles de claves. Un grupo limitado de trabajadores debe distribuir la carga uniformemente sin ser bloqueado por particiones pesadas. El soporte para reintentos y tareas retrasadas se implementa mediante Redis con scripts Lua en la biblioteca smart-redis-queue.

La solución utiliza la atomicidad de Lua para operaciones en ZSET, HASH y STRING sin condiciones de carrera. Las claves se forman con el prefijo queue:{queueName}:. Las particiones ordenadas (código con prefijo !) se procesan exclusivamente con bloqueo, mientras que las regulares se manejan en paralelo.

Comparación con Kafka y RabbitMQ

Kafka garantiza el orden dentro de las particiones de un tema y escala con grupos de consumidores, pero fija las asignaciones de particiones a consumidores. Un grupo dinámico de trabajadores que elige tareas de cualquier partición lista requiere semánticas diferentes. El TTL en Kafka no reemplaza la programación precisa hasta el tiempo T.

Google AdInline article slot

RabbitMQ soporta TTL y DLQ, pero el orden estricto con múltiples consumidores se rompe por round-robin. Colas separadas para millones de claves no escalan.

Redis + Lua ofrece una alternativa ligera: un viaje de ida y vuelta por operación atómica, sin necesidad de clúster o monitoreo complejo.

Estructura de Claves

| Clave | Estructura | Propósito |

Google AdInline article slot

|------|-----------|------------|

| queue:{nombre}:particiones | SET | Particiones activas |

| queue:{nombre}:particion:{código}:{prioridad} | ZSET | Cola de tareas por prioridad, puntuación = tiempo de disponibilidad (ms) |

Google AdInline article slot

| queue:{nombre}:particion:{código}:prioridades | ZSET | Niveles de prioridad |

| queue:{nombre}:payload:{taskId} | STRING | Carga útil |

| queue:{nombre}:particion:{taskId} | STRING | Código de partición |

| queue:{nombre}:consumidor:{id}:tareas | HASH | Tareas en progreso |

| queue:{nombre}:particion:{código}:lock | STRING | Bloqueo para particiones ordenadas |

| queue:{nombre}:particion:{código}:block | STRING (TTL) | Bloqueo temporal (límite de tasa) |

Ciclo de Vida de la Tarea

  • Publicar: Adición idempotente con SET NX, escribiendo en ZSET con puntuación = programado (ms). La partición se añade al SET.
local ok = redis.call('SET', payloadKey, payload, 'NX')
if ok then
    redis.call('ZADD', partitionQueueKey, scheduled, taskId)
    redis.call('SADD', partitionsKey, partitionCode)
end
  • Obtener: Latido del corazón, iteración sobre particiones, verificando bloqueo/block para ordenadas. Tomar por prioridades descendentes (ZREVRANGE), cabeza de ZSET si programado <= ahora. Mover al HASH del consumidor.
  • Ack: Eliminar de HASH, carga útil, claves. Liberar bloqueo si contador ordenado = 0.
  • Rechazar: Devolver a ZSET con nueva puntuación. Para ordenadas — prioridad +1, reject_seq para desempate. El consumo rechaza todo el lote de una partición ordenada si la primera tarea es rechazada.

Puntos de Referencia de Rendimiento

Pruebas en Go 1.22+, Redis 5, Intel i7-13700H.

Publicación (tareas/s):

  • 1 tarea: 17–23k
  • Lote 10: 80–92k
  • Lote 100: 100–117k

El procesamiento por lotes aumenta el rendimiento mediante una única llamada Lua.

Consumo por un trabajador (tareas/s):

| Prefetch | ns/op | Tareas/s |

|----------|--------|---------|

| 1 | 80–103 µs | 9.7–12.5k |

| 5 | 57–67 µs | 15–17.5k |

En paralelo:

  • 2 trabajadores: 28–30k/s
  • 8 trabajadores: 23–26k/s

La escalabilidad no es lineal debido a la contención en Redis.

Manejo de Fallos

  • Latido del corazón con TTL ~120s.
  • El consumidor vivo escanea los muertos mediante bloqueo distribuido.
  • Las tareas del HASH del consumidor muerto se devuelven a la cola (ordenadas con +prioridad).

Límite de tasa: RejectWithDelay establece un bloque con TTL.

Recomendación: pasos de prioridad de 100+ para evitar colisiones con incrementos de rechazo.

Conclusiones Clave

  • La atomicidad de Lua garantiza que no haya condiciones de carrera al mover tareas entre ZSET y HASH.
  • Las particiones ordenadas mantienen el orden mediante bloqueo exclusivo y lógica de rechazo.
  • El procesamiento por lotes en Publicar/Obtener aumenta el rendimiento a más de 100k tareas/s.
  • Recuperación automática tras fallos de trabajadores sin pérdida de tareas.
  • Adecuado para escenarios con millones de particiones sin sobrecarga de clúster.

— Editorial Team

Advertisement 728x90

Leer después