Retour à l'accueil

File d'attente Redis Lua : ordre de partition et pool de workers

L'article décrit l'implémentation d'une file d'attente de tâches sur Redis avec des scripts Lua pour garantir l'ordre dans les partitions avec un pool de workers partagé. Support des messages différés, réessais, benchmarks de performance. Adapté aux services à haute charge sans Kafka.

Scripts Lua dans Redis : file d'attente avec ordre et délai
Advertisement 728x90

File d'attente Redis avec Lua : Ordre garanti dans les partitions avec un pool de travailleurs partagé

Dans les systèmes à forte charge, le traitement des messages nécessite un ordre strict au sein des partitions logiques sur des centaines de milliers de clés. Un pool limité de travailleurs doit répartir uniformément la charge sans être bloqué par des partitions lourdes. La prise en charge des nouvelles tentatives et des tâches différées est implémentée via Redis avec des scripts Lua dans la bibliothèque smart-redis-queue.

La solution utilise l'atomicité de Lua pour les opérations sur ZSET, HASH et STRING sans conditions de concurrence. Les clés sont formées avec le préfixe queue:{queueName}:. Les partitions ordonnées (code avec préfixe !) sont traitées exclusivement avec verrouillage, tandis que les partitions régulières sont gérées en parallèle.

Comparaison avec Kafka et RabbitMQ

Kafka garantit l'ordre au sein des partitions de sujets et s'adapte avec des groupes de consommateurs, mais il fixe les affectations de partitions aux consommateurs. Un pool dynamique de travailleurs qui récupère des tâches depuis n'importe quelle partition prête nécessite une sémantique différente. La TTL dans Kafka ne remplace pas une planification précise jusqu'au temps T.

Google AdInline article slot

RabbitMQ prend en charge la TTL et la DLQ, mais l'ordre strict avec plusieurs consommateurs est rompu par le round-robin. Des files séparées pour des millions de clés ne sont pas évolutives.

Redis + Lua offre une alternative légère : un aller-retour par opération atomique, pas besoin de cluster ou de surveillance complexe.

Structure des clés

| Clé | Structure | Objectif |

Google AdInline article slot

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

| queue:{name}:partitions | SET | Partitions actives |

| queue:{name}:partition:{code}:{priority} | ZSET | File de tâches par priorité, score = temps de disponibilité (ms) |

Google AdInline article slot

| queue:{name}:partition:{code}:priorities | ZSET | Niveaux de priorité |

| queue:{name}:payload:{taskId} | STRING | Charge utile |

| queue:{name}:partition:{taskId} | STRING | Code de partition |

| queue:{name}:consumer:{id}:tasks | HASH | Tâches en cours |

| queue:{name}:partition:{code}:lock | STRING | Verrou pour les partitions ordonnées |

| queue:{name}:partition:{code}:block | STRING (TTL) | Blocage temporaire (limite de débit) |

Cycle de vie d'une tâche

  • Publication : Ajout idempotent avec SET NX, écriture dans ZSET avec score = planifié (ms). La partition est ajoutée au SET.
local ok = redis.call('SET', payloadKey, payload, 'NX')
if ok then
    redis.call('ZADD', partitionQueueKey, scheduled, taskId)
    redis.call('SADD', partitionsKey, partitionCode)
end
  • Récupération : Battement de cœur, itération sur les partitions, vérification du verrou/blocage pour les ordonnées. Prise par priorités décroissantes (ZREVRANGE), tête de ZSET si planifié <= maintenant. Déplacement vers le HASH du consommateur.
  • Accusé de réception : Suppression du HASH, de la charge utile, des clés. Libération du verrou si compteur ordonné = 0.
  • Rejet : Retour à ZSET avec un nouveau score. Pour les ordonnées — priorité +1, reject_seq pour départager. Le rejet de consommation rejette tout le lot d'une partition ordonnée si la première tâche est rejetée.

Benchmarks de performance

Tests sur Go 1.22+, Redis 5, Intel i7-13700H.

Publication (tâches/s) :

  • 1 tâche : 17–23k
  • Lot de 10 : 80–92k
  • Lot de 100 : 100–117k

Le traitement par lots augmente le débit via un seul appel Lua.

Consommation par un travailleur (tâches/s) :

| Préchargement | ns/op | Tâches/s |

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

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

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

En parallèle :

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

La mise à l'échelle est non linéaire en raison de la contention Redis.

Gestion des défaillances

  • Battement de cœur avec TTL ~120s.
  • Le consommateur vivant scanne les morts via un verrou distribué.
  • Les tâches du HASH du consommateur mort sont renvoyées dans la file (ordonnées avec +priorité).

Limite de débit : RejectWithDelay définit un bloc avec TTL.

Recommandation : pas de priorité de 100+ pour éviter les collisions avec les incréments de rejet.

Points clés à retenir

  • L'atomicité de Lua garantit l'absence de conditions de concurrence lors du déplacement des tâches entre ZSET et HASH.
  • Les partitions ordonnées maintiennent l'ordre via un verrou exclusif et une logique de rejet.
  • Le traitement par lots dans Publication/Récupération augmente le débit à 100k+ tâches/s.
  • Récupération automatique après défaillance des travailleurs sans perte de tâches.
  • Adapté aux scénarios avec des millions de partitions sans surcharge de cluster.

— Editorial Team

Advertisement 728x90

Lire ensuite