Redis Streams, de forma nativa: registros de solo anexado, grupos de consumidores y lecturas bloqueantes
Zaris ya habla en colecciones: hashes, listas, conjuntos y conjuntos ordenados que viven en el servidor y se replican como cualquier otra clave. Los Streams son el quinto tipo, y son el que convierte a Zaris de un almacén en una pieza de infraestructura de mensajería.
Un stream de Zaris es un verdadero registro de solo anexado: las entradas obtienen IDs monótonos, puedes leer rangos y puedes adjuntar grupos de consumidores para que un grupo de trabajadores se reparta el registro entre ellos, haga seguimiento de las entradas pendientes por consumidor y confirme lo que ha procesado. También puedes hacer una lectura bloqueante: un consumidor se detiene y espera a que lleguen nuevas entradas en lugar de iterar en caliente.
Y como es una colección nativa de Zaris, el registro está particionado y replicado exactamente como todo lo demás. Un stream sobrevive a la conmutación por error de un nodo, que es la razón misma para recurrir a la mensajería duradera en primer lugar.
El registro, desde .NET
Los streams tienen su propia superficie tipada: client.Streams.Stream(key) devuelve un manejador a un registro del lado del servidor. Un productor anexa entradas como conjuntos de campos; un grupo de consumidores lee y confirma:
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 entrega cada entrada a exactamente un consumidor del grupo y la mueve a la lista de pendientes de ese consumidor hasta que se confirma. Si worker-1 se bloquea antes de llamar a AckAsync, la entrada permanece pendiente y puede reclamarse: ese es el mecanismo detrás de la entrega al-menos-una-vez. Un ReadAsync simple (sin grupo) lee el registro directamente, y una lectura bloqueante permite a un consumidor esperar a la siguiente entrada en lugar de sondear.
El mismo registro, desde cualquier cliente de Redis
Todo lo anterior también es accesible a través del front-end RESP como los comandos estándar X de Redis Streams, operando sobre el mismo stream del lado del servidor:
# 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 (incluido BLOCK para lecturas bloqueantes), XREADGROUP, XACK y XPENDING se asignan todos al mismo stream nativo en el que escribe client.Streams. Un productor de .NET llamando a AddAsync y un trabajador de Python emitiendo XREADGROUP comparten un registro en una partición.
Para asegurarnos de que esto es comportamiento genuino de Redis Streams y no un imitador, ejecutamos un arnés de extremo a extremo que impulsa StackExchange.Redis contra Zaris sobre un socket real —el registro de solo anexado, los grupos de consumidores y las lecturas bloqueantes— y pasa 19/19 comprobaciones de interoperabilidad. Un cliente estándar de Redis Streams funciona sin modificaciones.
Qué construirías con ello
Un registro replicado y consciente de los grupos de consumidores es el sustrato para un puñado de patrones que, de otro modo, implican añadir un sistema separado:
- Event sourcing. Anexa cada cambio de estado como una entrada inmutable; el stream es la fuente de verdad, y las lecturas de rango reproducen el historial.
- Colas de trabajo con entrega al-menos-una-vez. Un grupo de consumidores reparte el registro entre los trabajadores; las entradas permanecen pendientes hasta que se confirman, así que el trabajo de un trabajador que se bloquea es reentregable en lugar de perderse.
- Fan-out. Varios grupos de consumidores leen cada uno todo el stream de forma independiente: un registro, muchas canalizaciones aguas abajo, cada una siguiendo su propia posición.
- Mensajería duradera entre servicios. Como el stream está replicado, un mensaje que un productor confirmó sobrevive a la conmutación por error de un nodo en lugar de desvanecerse con el nodo que casualmente lo tenía.
Ese último punto es donde un stream nativo y replicado se gana su sustento. La mensajería construida sobre un registro no replicado tiene un modo de fallo silencioso: el nodo muere y el backlog en vuelo muere con él, llevándose cada entrada sin confirmar. Un stream de Zaris se coloca y se replica como cualquier otra colección, así que el registro —y el estado pendiente que lo acompaña— sigue ahí al otro lado de una conmutación por error. La posición de lectura del grupo de consumidores y sus entradas pendientes sobresalientes también sobreviven, lo que significa que un trabajador que se reconecta tras la conmutación por error retoma exactamente donde el grupo lo dejó en lugar de reproducir desde el principio.
Nota honesta: al-menos-una-vez, no exactamente-una-vez
Esta es semántica de Redis Streams, y eso significa entrega al-menos-una-vez, no exactamente-una-vez.
Los grupos de consumidores más las confirmaciones garantizan que una entrada que no se confirma puede reentregarse: un trabajador no descartará trabajo en silencio. Lo que no garantizan es que una entrada dada se procese exactamente una vez. Si un trabajador termina su efecto secundario pero se bloquea antes de que aterrice AckAsync/XACK, la entrada sigue pendiente y se volverá a repartir. Así que tus consumidores deberían ser idempotentes: ancla los efectos secundarios al ID de la entrada, o diséñalos de modo que reprocesar la misma entrada sea inofensivo. Construye para la reentrega y la garantía de entrega es exactamente la que quieres; asume exactamente-una-vez y te llevarás una sorpresa.
Dónde encaja
Los Streams completan el conjunto: cinco tipos de colección del lado del servidor —hashes, listas, conjuntos, conjuntos ordenados y ahora un registro de solo anexado duradero— todos particionados y replicados de la misma forma que tus claves. Si no has visto cómo funcionan los otros cuatro, el artículo de estructuras de datos cubre el modelo compartido sobre el que se construyen.
Si ya estás ejecutando Redis Streams, el cambio es casi gratuito: los comandos son los que conoces, el cliente es el que ya usas, y la diferencia aparece cuando un nodo se cae y tu registro sigue ahí.