Grenzen erkennen, bevor sie zum Problem werden
Redis eignet sich mit dem LPUSH/BRPOP-Muster gut als einfache Task-Queue, und Streams liefern mit Consumer Groups sogar Zustellgarantien und Nachrichten-Historie. Doch beide Ansaetze stossen an Grenzen bei komplexem Routing, garantierter Reihenfolge ueber viele Consumer hinweg oder sehr hohem Nachrichtendurchsatz, wo dedizierte Systeme wie RabbitMQ oder Kafka die robustere Wahl sind.
Inhaltsverzeichnis
- 1. Warum Redis fuer Queues attraktiv ist
- 2. Das LPUSH/BRPOP-Muster im Detail
- 3. Eine einfache Queue in PHP implementieren
- 4. Grenzen des LPUSH/BRPOP-Musters
- 5. Redis Streams als robustere Warteschlange
- 6. Consumer Groups in PHP nutzen
- 7. Wo auch Streams an ihre Grenzen stossen
- 8. Wann RabbitMQ oder Kafka die bessere Wahl sind
- 9. Redis, RabbitMQ und Kafka im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Redis fuer Queues attraktiv ist
Redis wird in sehr vielen Systemen als Message Queue eingesetzt, obwohl es ursprünglich als In-Memory-Datenstruktur-Server konzipiert wurde, nicht als dedizierte Warteschlangenlösung. Der Grund für diese Popularität liegt in der Einfachheit: Wer bereits Redis für Caching betreibt, kann mit denselben Bordmitteln, insbesondere Listen und seit Version 5 auch Streams, eine funktionierende Task-Queue realisieren, ohne eine zusätzliche Infrastrukturkomponente wie RabbitMQ oder Kafka betreiben zu müssen.
Diese Einfachheit hat einen Preis. Redis als Message Queue bietet weder das ausgefeilte Routing von RabbitMQ mit Exchanges und Bindings noch die Partitionierungs- und Replay-Fähigkeiten von Kafka in vollem Umfang. Für kleine bis mittelgroße Systeme mit überschaubarem Nachrichtendurchsatz ist Redis dennoch eine legitime, oft sogar bevorzugte Wahl, weil die operative Komplexität deutlich geringer bleibt als bei einem dedizierten Message-Broker. Die folgenden Abschnitte zeigen sowohl die praktikablen Muster als auch die Punkte, an denen Redis als Message Queue an seine Grenzen stößt.
2. Das LPUSH/BRPOP-Muster im Detail
Die einfachste Form einer Redis-basierten Warteschlange nutzt zwei Listenbefehle: LPUSH fügt ein neues Element am Kopf einer Liste ein, BRPOP entfernt blockierend ein Element vom Ende derselben Liste. Producer rufen LPUSH queue:tasks payload auf, um eine Aufgabe einzustellen, Worker rufen BRPOP queue:tasks 0 auf, um blockierend auf die nächste Aufgabe zu warten, wobei der Timeout-Wert 0 für unbegrenztes Warten steht. Dieses Muster implementiert eine First-In-First-Out-Warteschlange mit minimalem Overhead und praktisch keiner Konfiguration.
Der entscheidende Vorteil von BRPOP gegenüber Polling mit RPOP in einer Schleife: Die Blockierung erfolgt serverseitig in Redis, ohne dass der Worker-Prozess aktiv Anfragen im Sekundentakt senden muss. Sobald ein Element verfügbar wird, liefert Redis es sofort an den wartenden Client aus, was eine Latenz von wenigen Millisekunden zwischen LPUSH und BRPOP-Rückgabe ermöglicht, ganz ohne Polling-Overhead.
# Producer: Aufgabe in die Queue einstellen
redis-cli LPUSH queue:emails '{"to":"kunde@example.com","template":"order_confirmed"}'
# Worker: blockierend auf die naechste Aufgabe warten (0 = kein Timeout)
redis-cli BRPOP queue:emails 0
# 1) "queue:emails"
# 2) "{\"to\":\"kunde@example.com\",\"template\":\"order_confirmed\"}"
# Queue-Laenge als Backlog-Indikator pruefen
redis-cli LLEN queue:emails
3. Eine einfache Queue in PHP implementieren
In PHP wird ein Worker für das LPUSH/BRPOP-Muster üblicherweise als langlebiger Prozess implementiert, der in einer Endlosschleife blockierend auf neue Aufgaben wartet. Wichtig ist eine saubere Fehlerbehandlung pro Aufgabe: Ein Fehler bei der Verarbeitung einer einzelnen Nachricht darf den Worker-Prozess nicht zum Absturz bringen, sonst bleiben alle nachfolgenden Aufgaben in der Queue unbearbeitet liegen.
Ein kritischer Schwachpunkt dieses einfachen Musters wird im nächsten Abschnitt behandelt: Sobald BRPOP ein Element aus der Liste entfernt hat, existiert keine Kopie mehr in Redis. Stürzt der Worker während der Verarbeitung ab, ist die Aufgabe unwiederbringlich verloren, ohne dass ein anderer Worker sie übernehmen könnte.
<?php
declare(strict_types=1);
final class EmailQueueWorker
{
public function __construct(private readonly \Predis\Client $redis)
{
}
/**
* Blocking worker loop consuming tasks from a Redis list queue.
*/
public function run(): void
{
while (true) {
// BRPOP blocks server-side until an item is available
$result = $this->redis->brpop(['queue:emails'], 0);
[$queueName, $payload] = $result;
try {
$task = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
$this->sendEmail($task['to'], $task['template']);
} catch (\Throwable $e) {
// Do not let a single bad task crash the worker
error_log("Task processing failed: " . $e->getMessage());
}
}
}
private function sendEmail(string $to, string $template): void
{
// Actual mail dispatch logic goes here
}
}
4. Grenzen des LPUSH/BRPOP-Musters
Das zentrale strukturelle Problem des LPUSH/BRPOP-Musters ist fehlende Zustellsicherheit. Sobald BRPOP ein Element zurückgibt, ist es unwiderruflich aus der Liste entfernt, es gibt keine eingebaute Bestätigungslogik. Stürzt der Worker zwischen Empfang und vollständiger Verarbeitung ab, ist die Aufgabe verloren, ohne Spur in Redis und ohne Möglichkeit, sie erneut zuzustellen. Für unkritische Aufgaben wie das Versenden einer Bestätigungs-E-Mail mag ein gelegentlicher Verlust tolerierbar sein, für Zahlungsabwicklung oder Bestandsänderungen ist er es nicht.
Eine zweite Einschränkung betrifft die Skalierung mit mehreren Consumer-Typen: Jede Nachricht wird genau einmal von genau einem Worker konsumiert. Wenn sowohl ein E-Mail-Versand-Service als auch ein Analytics-Service dieselbe Nachricht verarbeiten sollen, ist mit einer einzelnen Liste keine sinnvolle Fan-Out-Verteilung möglich, ohne die Nachricht in mehrere separate Listen zu duplizieren, was zusätzliche Producer-Logik und Koordinationsaufwand erfordert. Redis Streams lösen genau diese beiden Probleme strukturell.
5. Redis Streams als robustere Warteschlange
Redis Streams, eingeführt in Version 5, sind eine append-only Log-Datenstruktur, die für Message-Queue-Anwendungsfälle deutlich besser geeignet ist als einfache Listen. Jede Nachricht erhält beim Einfügen mit XADD eine eindeutige, monoton steigende ID, die aus Zeitstempel und Sequenznummer besteht. Nachrichten bleiben im Stream erhalten, auch nachdem sie gelesen wurden, was Replay und nachträgliche Auswertung ermöglicht, ein fundamentaler Unterschied zu Listen, bei denen ein gelesenes Element sofort verschwindet.
Der entscheidende Mechanismus für zuverlässige Verarbeitung sind Consumer Groups. Mehrere Worker registrieren sich als Gruppe an einem Stream, jede Nachricht wird genau einem Mitglied der Gruppe zugeteilt, aber die Nachricht bleibt im Stream als "pending" markiert, bis der Worker sie explizit mit XACK bestätigt. Stürzt ein Worker ab, ohne XACK zu senden, kann ein anderer Worker die Nachricht mit XCLAIM übernehmen und erneut verarbeiten. Diese Kombination aus Zustellgarantie und Fan-Out-Fähigkeit über mehrere Consumer Groups macht Streams zur deutlich robusteren Wahl für alles, was über einfache Fire-and-Forget-Aufgaben hinausgeht.
# Nachricht in einen Stream einfuegen, ID wird automatisch generiert
redis-cli XADD orders:stream '*' orderId 8842 status shipped
# Consumer Group erstellen, ab dem aktuellen Ende des Streams lesen
redis-cli XGROUP CREATE orders:stream email-workers '$'
# Nachricht innerhalb der Gruppe konsumieren
redis-cli XREADGROUP GROUP email-workers worker-1 COUNT 1 STREAMS orders:stream '>'
# Nach erfolgreicher Verarbeitung bestaetigen
redis-cli XACK orders:stream email-workers 1690000000000-0
# Unbestaetigte Nachrichten eines abgestuerzten Workers uebernehmen
redis-cli XCLAIM orders:stream email-workers worker-2 30000 1690000000000-0
6. Consumer Groups in PHP nutzen
Die Implementierung eines Stream-Workers mit Consumer Groups in PHP folgt einem klaren Ablauf: Nachrichten lesen, verarbeiten, bestätigen. Wichtig ist, das XACK erst nach vollständig erfolgreicher Verarbeitung aufzurufen, damit ein Absturz während der Verarbeitung dazu führt, dass die Nachricht als "pending" markiert bleibt und später von einem anderen Worker übernommen werden kann.
Ergänzend sollte ein separater, periodisch laufender Prozess mit XPENDING und XCLAIM nach Nachrichten suchen, die länger als ein definiertes Zeitfenster unbestätigt geblieben sind, und diese aktiv einem anderen Worker zuteilen. Ohne diesen zusätzlichen Mechanismus würden Nachrichten von abgestürzten Workern zwar nicht verloren gehen, aber auch nicht automatisch weiterverarbeitet werden.
<?php
declare(strict_types=1);
final class StreamOrderWorker
{
public function __construct(
private readonly \Predis\Client $redis,
private readonly string $consumerName = 'worker-1'
) {
}
/**
* Consume order events from a Redis Stream via a consumer group.
*/
public function run(): void
{
while (true) {
$messages = $this->redis->xreadgroup(
'order-workers',
$this->consumerName,
['orders:stream' => '>'],
1,
5000
);
if (empty($messages)) {
continue;
}
foreach ($messages['orders:stream'] as $id => $fields) {
try {
$this->processOrder($fields);
// Only ACK after successful processing
$this->redis->xack('orders:stream', 'order-workers', [$id]);
} catch (\Throwable $e) {
error_log("Order processing failed for {$id}: " . $e->getMessage());
// Message stays pending and can be reclaimed later
}
}
}
}
private function processOrder(array $fields): void
{
// Actual order processing logic goes here
}
}
7. Wo auch Streams an ihre Grenzen stossen
Redis Streams lösen die Zustellgarantie- und Fan-Out-Probleme von einfachen Listen, bleiben aber dennoch begrenzt gegenüber dedizierten Message-Broker-Systemen. Komplexes Routing, etwa themenbasierte Zustellung mit mehreren Bedingungen oder Prioritäten zwischen verschiedenen Nachrichtentypen, ist mit Streams nur durch zusätzliche Anwendungslogik nachbildbar, während RabbitMQ dafür mit Exchange-Typen wie Topic oder Headers eingebaute Unterstützung bietet.
Auch bei sehr hohem Durchsatz über mehrere Millionen Nachrichten pro Sekunde und horizontaler Partitionierung über viele physische Knoten hinweg zeigt sich, dass Redis, selbst im Cluster-Modus, konzeptionell nicht für diese Größenordnung entworfen wurde. Kafka wurde von Grund auf für genau dieses Szenario entwickelt, mit Partitionierung, Replikation und einer Speicherarchitektur, die auf Disk statt In-Memory setzt und dadurch deutlich größere Nachrichten-Historien wirtschaftlich vorhalten kann.
# Stream-Groesse begrenzen, um Speicherwachstum zu kontrollieren
redis-cli XTRIM orders:stream MAXLEN 100000
# Unbestaetigte Nachrichten aelter als 30 Sekunden auflisten
redis-cli XPENDING orders:stream order-workers IDLE 30000 - + 10
# Speicherverbrauch eines Streams pruefen
redis-cli MEMORY USAGE orders:stream
8. Wann RabbitMQ oder Kafka die bessere Wahl sind
RabbitMQ empfiehlt sich, sobald komplexes Routing benötigt wird: mehrere Consumer-Typen, die auf Basis von Routing-Keys oder Nachrichteneigenschaften unterschiedliche Teilmengen derselben Nachrichten erhalten sollen, priorisierte Warteschlangen oder Dead-Letter-Exchanges für systematische Fehlerbehandlung. Auch wenn strikte AMQP-Kompatibilität mit bestehenden Enterprise-Systemen gefordert ist, ist RabbitMQ die naheliegendere Wahl gegenüber Redis.
Kafka wird relevant, sobald Nachrichtendurchsatz und Retention-Dauer die Grenzen von Redis sprengen: Event-Sourcing-Architekturen, bei denen Jahre an Historie durchsuchbar bleiben müssen, Streaming-Analytics mit mehreren unabhängigen Consumer-Gruppen, die denselben Datenstrom parallel und mit unterschiedlicher Geschwindigkeit verarbeiten, oder Systeme mit garantierter, strikter Nachrichtenreihenfolge über Partitionsgrenzen hinweg. Für die meisten Web-Anwendungen mit überschaubarem Nachrichtenvolumen bleibt Redis mit Streams jedoch die pragmatischere, operativ einfachere Wahl.
9. Redis, RabbitMQ und Kafka im direkten Vergleich
Die folgende Tabelle stellt die drei Systeme entlang der wichtigsten Entscheidungskriterien gegenueber.
| Kriterium | Redis (Streams) | RabbitMQ | Kafka |
|---|---|---|---|
| Routing-Komplexitaet | Einfach | Sehr flexibel | Einfach, ueber Topics |
| Durchsatz | Hoch, In-Memory-begrenzt | Mittel bis hoch | Sehr hoch, disk-basiert |
| Retention | Begrenzt durch RAM | Bis zur Verarbeitung | Konfigurierbar, oft Tage bis Jahre |
| Operativer Aufwand | Gering, wenn Redis schon vorhanden | Mittel | Hoch, eigene Cluster-Infrastruktur |
| Typischer Einsatz | Web-App-Task-Queues | Enterprise-Integration, Routing | Event-Sourcing, Streaming-Analytics |
Die Wahl zwischen den drei Systemen sollte anhand des tatsaechlichen Bedarfs erfolgen, nicht anhand von Popularitaet. Wer bereits Redis betreibt und moderate Anforderungen an Routing und Durchsatz hat, spart mit Streams erhebliche operative Komplexitaet gegenueber einem zusaetzlichen Message-Broker.
Mironsoft
Redis-Architektur, Message Queues und Systemintegration
Die richtige Warteschlangen-Loesung fuer euer System finden?
Wir bewerten euren Nachrichtendurchsatz und eure Zustellanforderungen und entscheiden gemeinsam, ob Redis Streams ausreichen oder ob RabbitMQ oder Kafka die richtige Wahl sind.
Bedarfsanalyse
Durchsatz, Routing- und Zustellanforderungen realistisch einschaetzen
Implementierung
Redis Streams mit Consumer Groups robust und produktionsreif umsetzen
Migration
Uebergang zu RabbitMQ oder Kafka planen, wenn Redis an Grenzen stoesst
10. Zusammenfassung
Redis als Message Queue funktioniert für viele Anwendungsfälle gut, mit unterschiedlicher Robustheit je nach gewähltem Muster. Das einfache LPUSH/BRPOP-Muster liefert eine funktionierende FIFO-Queue ohne Zustellgarantie, geeignet für unkritische, verlusttolerante Aufgaben. Redis Streams mit Consumer Groups schließen diese Lücke durch XACK-Bestätigung und XCLAIM-Wiederzuteilung und ermöglichen zusätzlich Fan-Out über mehrere Consumer-Gruppen sowie Nachrichten-Replay.
Für komplexes Routing, sehr hohen Durchsatz oder lange Retention-Zeiträume stoßen jedoch auch Streams an ihre Grenzen. RabbitMQ bietet in diesen Fällen flexibleres Routing über Exchanges, Kafka skaliert für Event-Sourcing und Streaming-Analytics deutlich weiter. Die richtige Entscheidung hängt von den konkreten Anforderungen an Durchsatz, Retention und Routing-Komplexität ab, nicht von der grundsätzlichen Frage, ob Redis "gut genug" für Queues ist.
Redis als Message Queue, das Wichtigste auf einen Blick
LPUSH/BRPOP
Einfache FIFO-Queue ohne Zustellgarantie. Gut fuer unkritische, verlusttolerante Aufgaben.
Streams & Consumer Groups
XACK und XCLAIM liefern Zustellgarantie, Fan-Out und Replay ueber mehrere Consumer-Gruppen.
Wann RabbitMQ
Bei komplexem Routing, priorisierten Queues oder Dead-Letter-Exchanges fuer systematische Fehlerbehandlung.
Wann Kafka
Bei sehr hohem Durchsatz, langer Retention oder Event-Sourcing mit vielen unabhaengigen Consumer-Gruppen.