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.
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 |
|------|-----------|------------|
| queue:{nombre}:particiones | SET | Particiones activas |
| queue:{nombre}:particion:{código}:{prioridad} | ZSET | Cola de tareas por prioridad, puntuación = tiempo de disponibilidad (ms) |
| 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
Aún no hay comentarios.