Aller au contenu principal

Redis Streams, nativement : journaux en ajout seul, groupes de consommateurs et lectures bloquantes

· 6 minutes de lecture
Clustron Team
Ingénierie des systèmes distribués

Un journal en ajout seul natif et répliqué, avec groupes de consommateurs et lectures bloquantes dans Zaris

Zaris s'exprime déjà en collections — hashes, listes, sets et sorted sets qui résident sur le serveur et se répliquent comme toute autre clé. Les Streams sont le cinquième type, et c'est celui qui fait passer Zaris du statut de store à celui d'élément d'infrastructure de messagerie.

Un stream Zaris est un véritable journal en ajout seul : les entrées reçoivent des ID monotones, vous pouvez lire des plages, et vous pouvez attacher des groupes de consommateurs pour qu'un pool de workers se partage le journal, suive les entrées en attente par consommateur et acquitte ce qu'il a traité. Vous pouvez aussi effectuer une lecture bloquante — un consommateur se met en pause et attend l'arrivée de nouvelles entrées au lieu de boucler à vide.

Et parce qu'il s'agit d'une collection Zaris native, le journal est partitionné et répliqué exactement comme tout le reste. Un stream survit au basculement d'un nœud, ce qui est précisément la raison pour laquelle on recourt à une messagerie durable en premier lieu.

Le journal, depuis .NET​

Les streams disposent de leur propre surface typée : client.Streams.Stream(key) renvoie un handle vers un journal côté serveur. Un producteur ajoute des entrées sous forme d'ensembles de champs ; un groupe de consommateurs lit et acquitte :

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 remet chaque entrée à exactement un consommateur du groupe et la déplace vers la liste des entrées en attente de ce consommateur jusqu'à ce qu'elle soit acquittée. Si worker-1 s'arrête brutalement avant d'appeler AckAsync, l'entrée reste en attente et peut être réclamée — c'est le mécanisme qui sous-tend la livraison au moins une fois. Un ReadAsync simple (sans groupe) lit directement le journal, et une lecture bloquante permet à un consommateur d'attendre l'entrée suivante plutôt que d'interroger en boucle.

Le même journal, depuis n'importe quel client Redis​

Tout ce qui précède est également accessible via le front-end RESP sous la forme des commandes standard X de Redis Streams, opérant sur le même stream côté serveur :

# 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 (y compris BLOCK pour les lectures bloquantes), XREADGROUP, XACK et XPENDING se projettent tous sur le même stream natif dans lequel écrit client.Streams. Un producteur .NET appelant AddAsync et un worker Python émettant XREADGROUP partagent un seul journal sur une seule partition.

Pour nous assurer qu'il s'agit bien d'un comportement authentique de Redis Streams et non d'un sosie, nous avons exécuté un harnais de bout en bout qui pilote StackExchange.Redis face à Zaris sur un véritable socket — le journal en ajout seul, les groupes de consommateurs et les lectures bloquantes — et il passe 19/19 vérifications d'interopérabilité. Un client Redis Streams standard fonctionne sans modification.

Ce que vous construiriez avec​

Un journal répliqué et conscient des groupes de consommateurs est le substrat d'une poignée de modèles qui, autrement, impliquent de greffer un système distinct :

  • Event sourcing. Ajoutez chaque changement d'état comme une entrée immuable ; le stream est la source de vérité, et les lectures de plage rejouent l'historique.
  • Files de travail avec livraison au moins une fois. Un groupe de consommateurs répartit le journal entre les workers ; les entrées restent en attente jusqu'à l'acquittement, de sorte que le travail d'un worker qui s'arrête brutalement est redélivrable plutôt que perdu.
  • Fan-out. Plusieurs groupes de consommateurs lisent chacun l'intégralité du stream indépendamment — un journal, de nombreux pipelines en aval, chacun suivant sa propre position.
  • Messagerie durable inter-services. Parce que le stream est répliqué, un message qu'un producteur a validé survit au basculement d'un nœud au lieu de s'évanouir avec le nœud qui se trouvait le détenir.

Ce dernier point est là où un stream natif et répliqué justifie son existence. Une messagerie construite sur un journal non répliqué comporte un mode de défaillance silencieux : le nœud meurt et l'arriéré en cours meurt avec lui, emportant chaque entrée non acquittée. Un stream Zaris est placé et répliqué comme toute autre collection, de sorte que le journal — et l'état des entrées en attente qui l'accompagne — est toujours là de l'autre côté d'un basculement. La position de lecture du groupe de consommateurs et ses entrées en attente non résolues survivent elles aussi, ce qui signifie qu'un worker qui se reconnecte après le basculement reprend exactement là où le groupe s'était arrêté plutôt que de rejouer depuis le début.

Note honnête : au moins une fois, pas exactement une fois​

Il s'agit de la sémantique de Redis Streams, ce qui signifie une livraison au moins une fois, pas exactement une fois.

Les groupes de consommateurs assortis d'acquittements garantissent qu'une entrée non acquittée peut être redélivrée — un worker ne laissera pas silencieusement tomber du travail. Ce qu'ils ne garantissent pas, c'est qu'une entrée donnée soit traitée exactement une fois. Si un worker achève son effet de bord mais s'arrête brutalement avant que AckAsync/XACK n'aboutisse, l'entrée reste en attente et sera redistribuée. Vos consommateurs devraient donc être idempotents : indexez les effets de bord sur l'ID d'entrée, ou concevez-les de sorte que retraiter la même entrée soit sans conséquence. Concevez pour la redélivrance et la garantie de livraison est exactement ce que vous voulez ; présumez le exactement-une-fois et vous serez surpris.

Où cela s'insère​

Les streams complètent l'ensemble : cinq types de collection côté serveur — hashes, listes, sets, sorted sets, et désormais un journal durable en ajout seul — tous partitionnés et répliqués de la même façon que le sont vos clés. Si vous n'avez pas vu comment fonctionnent les quatre autres, l'article sur les structures de données couvre le modèle partagé sur lequel ils sont construits.

Si vous exécutez déjà Redis Streams, le passage est quasiment gratuit : les commandes sont celles que vous connaissez, le client est celui que vous utilisez déjà, et la différence apparaît quand un nœud tombe et que votre journal est toujours là.