Redis Streams nativ: Append-only-Logs, Consumer Groups und blockierende Lesezugriffe
Zaris spricht bereits in Collections — Hashes, Listen, Sets und Sorted Sets, die auf dem Server liegen und wie jeder andere Key repliziert werden. Streams sind die fünfte Art, und sie ist diejenige, die aus Zaris vom Store ein Stück Messaging-Infrastruktur macht.
Ein Zaris-Stream ist ein echtes Append-only-Log: Einträge erhalten monoton steigende IDs, Sie können Bereiche lesen, und Sie können Consumer Groups anbinden, sodass ein Pool von Workern das Log untereinander aufteilt, pro Consumer ausstehende Einträge verfolgt und bestätigt, was verarbeitet wurde. Sie können außerdem einen blockierenden Lesezugriff ausführen — ein Consumer parkt und wartet, bis neue Einträge eintreffen, statt in einer Schleife zu hämmern.
Und weil es eine native Zaris-Collection ist, wird das Log genau wie alles andere partitioniert und repliziert. Ein Stream übersteht ein Node-Failover, und genau dafür greift man überhaupt erst zu dauerhaftem Messaging.
Das Log aus .NET
Streams haben eine eigene typisierte Oberfläche: client.Streams.Stream(key) liefert ein Handle auf ein serverseitiges Log. Ein Producer hängt Einträge als Feldmengen an; eine Consumer Group liest und bestätigt:
var orders = client.Streams.Stream("orders");
// Producer — append an entry (returns its generated ID)
await orders.AppendAsync(new { id = 1001, total = 59.90, status = "placed" });
// Set up a consumer group once
await orders.CreateGroupAsync("fulfilment");
// Worker — read as part of the group, process, then acknowledge
var batch = await orders.ReadGroupAsync("fulfilment", "worker-1", count: 10);
foreach (var entry in batch)
{
Process(entry);
await orders.AckAsync("fulfilment", entry.Id);
}
ReadGroupAsync übergibt jeden Eintrag genau einem Consumer der Gruppe und verschiebt ihn in die Pending-Liste dieses Consumers, bis er bestätigt wird. Stürzt worker-1 ab, bevor er AckAsync aufruft, bleibt der Eintrag ausstehend und kann zurückgefordert werden — das ist der Mechanismus hinter At-least-once-Zustellung. Ein einfaches ReadAsync (ohne Gruppe) liest das Log direkt, und ein blockierender Lesezugriff lässt einen Consumer auf den nächsten Eintrag warten, statt zu pollen.
Dasselbe Log von jedem Redis-Client
Alles oben Gezeigte ist auch über das RESP-Frontend als die Standard-X-Befehle von Redis Streams erreichbar und arbeitet auf demselben serverseitigen Stream:
# Append entries (* = server-assigned ID)
redis-cli -h zaris-0 -p 6379 XADD orders '*' id 1001 total 59.90 status placed
redis-cli -h zaris-0 -p 6379 XLEN orders
redis-cli -h zaris-0 -p 6379 XRANGE orders - +
# Consumer groups
redis-cli -h zaris-0 -p 6379 XGROUP CREATE orders fulfilment 0
redis-cli -h zaris-0 -p 6379 XREADGROUP GROUP fulfilment worker-1 COUNT 10 STREAMS orders '>'
redis-cli -h zaris-0 -p 6379 XACK orders fulfilment 1526919030474-0
redis-cli -h zaris-0 -p 6379 XPENDING orders fulfilment
XREAD (einschließlich BLOCK für blockierende Lesezugriffe), XREADGROUP, XACK und XPENDING werden alle auf denselben nativen Stream abgebildet, in den client.Streams schreibt. Ein .NET-Producer, der AddAsync aufruft, und ein Python-Worker, der XREADGROUP absetzt, teilen sich ein Log auf einer Partition.
Um sicherzustellen, dass dies echtes Redis-Streams-Verhalten ist und kein Imitat, haben wir ein End-to-End-Harness laufen lassen, das StackExchange.Redis gegen Zaris über einen echten Socket betreibt — das Append-only-Log, Consumer Groups und blockierende Lesezugriffe — und es besteht 19/19 Interoperabilitätsprüfungen. Ein Standard-Redis-Streams-Client funktioniert unverändert.
Was Sie damit bauen können
Ein repliziertes, Consumer-Group-fähiges Log ist das Fundament für eine Handvoll Muster, die sonst bedeuten würden, ein separates System anzuflanschen:
- Event Sourcing. Hängen Sie jede Zustandsänderung als unveränderlichen Eintrag an; der Stream ist die Quelle der Wahrheit, und Bereichslesezugriffe spielen die Historie erneut ab.
- Work Queues mit At-least-once-Zustellung. Eine Consumer Group teilt das Log auf Worker auf; Einträge bleiben ausstehend, bis sie bestätigt sind, sodass die Arbeit eines abgestürzten Workers erneut zustellbar ist, statt verloren zu gehen.
- Fan-out. Mehrere Consumer Groups lesen jeweils den gesamten Stream unabhängig — ein Log, viele nachgelagerte Pipelines, jede mit eigener Position.
- Dauerhaftes Messaging zwischen Diensten. Weil der Stream repliziert ist, übersteht eine vom Producer committete Nachricht ein Node-Failover, statt mit dem Node zu verschwinden, der sie zufällig hielt.
Dieser letzte Punkt ist, wo sich ein nativer, replizierter Stream bezahlt macht. Messaging auf einem nicht replizierten Log hat einen stillen Fehlermodus: Der Node stirbt, und der Backlog in Bearbeitung stirbt mit ihm und reißt jeden unbestätigten Eintrag mit. Ein Zaris-Stream wird wie jede andere Collection platziert und repliziert, sodass das Log — und der dazugehörige Pending-Zustand — auch nach einem Failover noch da ist. Auch die Leseposition der Consumer Group und ihre ausstehenden Einträge bleiben erhalten, was bedeutet, dass ein Worker, der sich nach dem Failover neu verbindet, genau dort weitermacht, wo die Gruppe aufgehört hat, statt von vorn abzuspielen.
Ehrlicher Hinweis: At-least-once, nicht Exactly-once
Dies sind Redis-Streams-Semantiken, und das bedeutet At-least-once-Zustellung, nicht Exactly-once.
Consumer Groups plus Bestätigungen garantieren, dass ein Eintrag, der nicht bestätigt wurde, erneut zugestellt werden kann — ein Worker verliert Arbeit nicht stillschweigend. Was sie nicht garantieren, ist, dass ein bestimmter Eintrag genau einmal verarbeitet wird. Beendet ein Worker seinen Seiteneffekt, stürzt aber ab, bevor AckAsync/XACK ankommt, ist der Eintrag weiterhin ausstehend und wird erneut ausgegeben. Ihre Consumer sollten daher idempotent sein: Verknüpfen Sie Seiteneffekte mit der Entry-ID oder gestalten Sie sie so, dass die erneute Verarbeitung desselben Eintrags harmlos ist. Bauen Sie für Redelivery, und die Zustellgarantie ist genau das, was Sie wollen; gehen Sie von Exactly-once aus, werden Sie überrascht sein.
Wo es hinpasst
Streams vervollständigen das Set: fünf serverseitige Collection-Arten — Hashes, Listen, Sets, Sorted Sets und nun ein dauerhaftes Append-only-Log —, alle auf dieselbe Weise partitioniert und repliziert wie Ihre Keys. Wenn Sie noch nicht gesehen haben, wie die anderen vier funktionieren, behandelt der Beitrag zu Datenstrukturen das gemeinsame Modell, auf dem sie aufbauen.
Wenn Sie bereits Redis Streams betreiben, ist der Umstieg nahezu kostenlos: Die Befehle sind die, die Sie kennen, der Client ist der, den Sie bereits nutzen, und der Unterschied zeigt sich, wenn ein Node ausfällt und Ihr Log trotzdem noch da ist.