Redis-Warteschlange mit Lua: Garantierte Reihenfolge in Partitionen mit einem gemeinsamen Worker-Pool
In Hochlastsystemen erfordert die Nachrichtenverarbeitung eine strikte Reihenfolge innerhalb logischer Partitionen über Hunderttausende von Schlüsseln hinweg. Ein begrenzter Worker-Pool muss die Last gleichmäßig verteilen, ohne durch schwere Partitionen blockiert zu werden. Unterstützung für Wiederholungen und verzögerte Aufgaben wird über Redis mit Lua-Skripten in der smart-redis-queue-Bibliothek implementiert.
Die Lösung nutzt die Atomarität von Lua für Operationen auf ZSET, HASH und STRING ohne Race Conditions. Schlüssel werden mit dem Präfix queue:{queueName}: gebildet. Geordnete Partitionen (Code mit Präfix !) werden exklusiv mit Locking verarbeitet, während reguläre parallel abgearbeitet werden.
Vergleich mit Kafka und RabbitMQ
Kafka gewährleistet Reihenfolge innerhalb von Topic-Partitionen und skaliert mit Consumer-Gruppen, fixiert jedoch die Partitionenzuweisungen an Consumer. Ein dynamischer Worker-Pool, der Aufgaben aus beliebigen bereiten Partitionen entnimmt, erfordert andere Semantiken. TTL in Kafka ersetzt keine präzise Zeitplanung bis Zeitpunkt T.
RabbitMQ unterstützt TTL und DLQ, aber die strikte Reihenfolge bei mehreren Consumern wird durch Round-Robin gebrochen. Separate Warteschlangen für Millionen von Schlüsseln skalieren nicht.
Redis + Lua bietet eine leichte Alternative: Ein Round-Trip pro atomarer Operation, kein Cluster oder komplexes Monitoring erforderlich.
Schlüsselstruktur
| Schlüssel | Struktur | Zweck |
|------|-----------|------------|
| queue:{name}:partitions | SET | Aktive Partitionen |
| queue:{name}:partition:{code}:{priority} | ZSET | Aufgabenwarteschlange nach Priorität, Score = Verfügbarkeitszeit (ms) |
| queue:{name}:partition:{code}:priorities | ZSET | Prioritätsstufen |
| queue:{name}:payload:{taskId} | STRING | Nutzdaten |
| queue:{name}:partition:{taskId} | STRING | Partitionscode |
| queue:{name}:consumer:{id}:tasks | HASH | Aufgaben in Bearbeitung |
| queue:{name}:partition:{code}:lock | STRING | Sperre für geordnete Partitionen |
| queue:{name}:partition:{code}:block | STRING (TTL) | Temporäre Blockade (Rate Limit) |
Aufgabenlebenszyklus
- Veröffentlichen: Idempotente Hinzufügung mit
SET NX, Schreiben in ZSET mit Score = geplant (ms). Partition wird zum SET hinzugefügt.
local ok = redis.call('SET', payloadKey, payload, 'NX')
if ok then
redis.call('ZADD', partitionQueueKey, scheduled, taskId)
redis.call('SADD', partitionsKey, partitionCode)
end
- Abrufen: Heartbeat, Iteration über Partitionen, Prüfung von Lock/Block für geordnete. Entnahme nach absteigenden Prioritäten (
ZREVRANGE), Kopf der ZSET wenn scheduled <= jetzt. Verschieben in HASH des Consumers.
- Bestätigen: Entfernen aus HASH, Nutzdaten, Schlüssel. Sperre freigeben, wenn geordneter Zähler = 0.
- Ablehnen: Zurück zu ZSET mit neuem Score. Für geordnete — Priorität +1, reject_seq für Tie-Break. Verarbeitung lehnt den gesamten Batch einer geordneten Partition ab, wenn die erste Aufgabe abgelehnt wird.
Leistungsbenchmarks
Tests mit Go 1.22+, Redis 5, Intel i7-13700H.
Veröffentlichung (Aufgaben/s):
- 1 Aufgabe: 17–23k
- Batch 10: 80–92k
- Batch 100: 100–117k
Batching erhöht den Durchsatz über einen einzigen Lua-Aufruf.
Verarbeitung durch einen Worker (Aufgaben/s):
| Prefetch | ns/op | Aufgaben/s |
|----------|--------|---------|
| 1 | 80–103 µs | 9.7–12.5k |
| 5 | 57–67 µs | 15–17.5k |
Parallel:
- 2 Worker: 28–30k/s
- 8 Worker: 23–26k/s
Skalierung ist nicht linear aufgrund von Redis-Konflikten.
Fehlerbehandlung
- Heartbeat mit TTL ~120s.
- Live-Consumer scannt tote über verteilte Sperre.
- Aufgaben aus HASH toter Consumer werden zurück in die Warteschlange gegeben (geordnete mit +Priorität).
Rate Limit: RejectWithDelay setzt Block mit TTL.
Empfehlung: Prioritätsschritte von 100+ um Kollisionen mit Ablehnungsinkrementen zu vermeiden.
Wichtige Erkenntnisse
- Lua-Atomarität gewährleistet keine Race Conditions beim Verschieben von Aufgaben zwischen ZSET und HASH.
- Geordnete Partitionen halten Reihenfolge über exklusive Sperre und Ablehnungslogik.
- Batching bei Veröffentlichung/Abruf steigert Durchsatz auf 100k+ Aufgaben/s.
- Automatische Wiederherstellung nach Worker-Ausfällen ohne Aufgabenverlust.
- Geeignet für Szenarien mit Millionen Partitionen ohne Cluster-Overhead.
— Editorial Team
Noch keine Kommentare.