Redis als Message Queue: Grenzen und Alternativen
AI generated
SET
TTL
Redis · Message Queue · Systemarchitektur
Redis als Message Queue
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.

19 Min. Lesezeit LPUSH/BRPOP · Streams · Consumer Groups · RabbitMQ · Kafka Redis 7.x · Predis · phpredis

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.

11. FAQ: Redis als Message Queue

1Ist Redis eine echte Message Queue?
Nicht klassisch, aber mit Listen und Streams lassen sich funktionierende Queue-Muster umsetzen.
2Hauptproblem von LPUSH/BRPOP?
Fehlende Zustellgarantie, ein Absturz waehrend der Verarbeitung verliert die Aufgabe.
3Wie loesen Streams das?
Pending-Markierung bis XACK, unbestaetigte Nachrichten uebernimmt ein anderer Worker per XCLAIM.
4Was ist eine Consumer Group?
Eine Gruppe Worker, die sich einen Stream teilt, mit Fan-Out ueber mehrere unabhaengige Gruppen.
5Wann RabbitMQ statt Redis?
Bei komplexem Routing, Prioritaeten oder Dead-Letter-Exchanges.
6Wann Kafka statt Redis?
Bei sehr hohem Durchsatz, langer Retention oder vielen unabhaengigen Consumer-Gruppen.
7Nachrichten erneut abspielen?
Mit Listen nein, mit Streams ja, solange kein Trimming stattgefunden hat.
8RAM begrenzt Retention?
Ja, Redis ist In-Memory, Kafka mit Disk-Speicher ermoeglicht deutlich laengere Historie.
9Umstieg aufwendig?
Haengt vom Abstraktionsgrad ab, mit klarer Interface-Schicht bleibt der Aufwand ueberschaubar.
10Beide Muster parallel nutzen?
Ja, ueblich: unkritische Aufgaben ueber Listen, kritische Events ueber Streams.