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.
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 |
|------|-----------|------------|
| queue:{name}:partitions | SET | Partitions actives |
| queue:{name}:partition:{code}:{priority} | ZSET | File de tâches par priorité, score = temps de disponibilité (ms) |
| 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
Aucun commentaire pour le moment.