Saltar al contenido principal

Estructuras de datos que se replican: hashes, listas, conjuntos y conjuntos ordenados en Zaris

· 7 min de lectura
Clustron Team
Ingeniería de Sistemas Distribuidos

Hashes, listas, conjuntos y conjuntos ordenados nativos, replicados como cualquier otra clave en Zaris

Zaris 1.x almacenaba valores. Zaris 2.0.0 almacena colecciones. Los hashes, listas, conjuntos y conjuntos ordenados son ahora tipos de primera clase, del lado del servidor, con APIs de .NET reales y tipadas y, lo que es igual de importante, se comportan como todo lo demás en el almacén.

Esa última parte es lo que la gente suele entender mal. En muchos sistemas, las "estructuras de datos" son una ilusión del lado del cliente: una biblioteca serializa un diccionario en un blob, lo hace Put y lo llama un hash. Funciona hasta que dos clientes tocan la misma colección a la vez, o hasta que te preguntas qué pasa en una conmutación por error. En Zaris, un hash es un hash en el servidor. Reside en la partición propietaria de su clave, se replica a la réplica de esa partición y sobrevive a la pérdida de un nodo exactamente igual que una clave simple.

Cuatro tipos, una API tipada​

Cada tipo de colección cuelga de client.DataStructures, con la clave que usarías para cualquier otra clave:

// Hash — field/value map
await client.DataStructures.Hash("user:42").SetAsync("email", "ada@example.com");
await client.DataStructures.Hash("user:42").SetAsync("plan", "pro");
var email = await client.DataStructures.Hash("user:42").GetAsync("email");
var all = await client.DataStructures.Hash("user:42").GetAllAsync();

// Sorted set — members ordered by score
await client.DataStructures.SortedSet("leaderboard").AddAsync("ada", score: 4096);
await client.DataStructures.SortedSet("leaderboard").AddAsync("grace", score: 8192);
var top = await client.DataStructures.SortedSet("leaderboard").RangeByRankAsync(0, 9);
var band = await client.DataStructures.SortedSet("leaderboard").RangeByScoreAsync(1000, 5000);

// List — ordered, push and range
await client.DataStructures.List("events:42").PushAsync("login");
var recent = await client.DataStructures.List("events:42").RangeAsync(0, 19);

// Set — unique membership
await client.DataStructures.Set("tags:42").AddAsync("beta");
var isMember = await client.DataStructures.Set("tags:42").IsMemberAsync("beta");
var members = await client.DataStructures.Set("tags:42").MembersAsync();

Nada de esto es un ayudante que obtiene un blob y lo muta en tu proceso. Hash("user:42").SetAsync(...) es una operación del lado del servidor contra la colección llamada user:42, de la misma manera que Put("user:42", ...) lo es contra el valor llamado user:42.

La misma colocación, la misma replicación, la misma conmutación por error​

Esta es la decisión de diseño que hace que estos tipos sean fiables: una operación de estructura de datos recorre exactamente la misma ruta replicada que la escritura de una clave ordinaria.

Cada colección es un único valor lógico que vive en la partición propietaria de su clave. Se coloca mediante el mismo mapa de particiones, está cubierta por el mismo factor de replicación y conmuta por error de la misma manera. No hay un "subsistema de colecciones" separado con su propia historia de durabilidad: si entiendes cómo se coloca y replica una clave de Zaris, ya entiendes cómo lo hace un conjunto ordenado.

Bajo el capó, una operación de mutación es una lectura-modificación-escritura de valor completo con compare-and-set (CAS) sobre la misma ruta replicada de Put/Get que usan las claves simples. Añadir un miembro a un conjunto ordenado lee la colección actual, aplica el cambio y lo confirma con un compare-and-set; si otro escritor llegó primero, el CAS falla y la operación se reintenta contra el valor fresco. Como la confirmación es el mismo Put replicado, la actualización es:

  • Atómica por clave: una única mutación de colección aterriza entera o no aterriza en absoluto.
  • Replicada a la réplica de la partición: el cambio es duradero frente a la pérdida de nodo que Zaris está diseñado para sobrevivir.
  • Consistente con las claves simples: una actualización de hash y una actualización de clave obtienen las mismas garantías, porque son la misma escritura.

Ese es todo el truco: en lugar de un modelo de consistencia a medida por tipo de dato, Zaris expresa cada mutación en términos de la única operación que ya replica correctamente.

Las mismas colecciones, desde cualquier cliente de Redis​

Cada uno de estos tipos también se expone a través del front-end del protocolo de Redis (RESP), de modo que un cliente de Redis en cualquier lenguaje opera sobre las mismas colecciones del lado del servidor, no sobre un conjunto paralelo que vive en otro sitio.

# Hash
redis-cli -h zaris-0 -p 6379 HSET user:42 email ada@example.com plan pro
redis-cli -h zaris-0 -p 6379 HGETALL user:42

# Sorted set
redis-cli -h zaris-0 -p 6379 ZADD leaderboard 4096 ada 8192 grace
redis-cli -h zaris-0 -p 6379 ZRANGE leaderboard 0 9

# List and set
redis-cli -h zaris-0 -p 6379 LPUSH events:42 login
redis-cli -h zaris-0 -p 6379 LRANGE events:42 0 19
redis-cli -h zaris-0 -p 6379 SADD tags:42 beta
redis-cli -h zaris-0 -p 6379 SMEMBERS tags:42

Un servicio de .NET escribiendo a través de client.DataStructures.Hash("user:42") y un servicio de Python emitiendo HSET user:42 están tocando el mismo hash en la misma partición. La capa RESP traduce el comando a la colección nativa; no es una segunda implementación.

Validamos esto entre lenguajes en lugar de simplemente afirmarlo: las suites de clientes multilenguaje pasan 38/38 en .NET, 51/51 en Java y 40/40 en Ruby, ejercitando la misma semántica de colección a través del cliente de cada lenguaje.

La concesión honesta: el CAS de valor completo​

El CAS de valor completo es lo que compra la garantía limpia, y también es donde reside el coste. Cada mutación lee y vuelve a escribir el valor completo de la colección, y los escritores concurrentes a la misma colección contienden en el compare-and-set: el perdedor se reintenta contra el nuevo valor.

En la práctica, estos tipos encajan muy bien con colecciones de tamaño pequeño a mediano, donde reescribir el valor completo es barato, y con patrones de muchas lecturas o escrituras moderadas, donde los reintentos de CAS son raros.

Donde lo notarías es en el extremo opuesto: una colección muy grande con muchos escritores concurrentes muy activos. Un hash gigante que decenas de clientes martillean a la vez pasará tiempo real perdiendo y reintentando compare-and-sets, y cada escritura paga por serializar el valor completo.

La solución es el movimiento habitual de los sistemas distribuidos: fragmentar la colección enorme entre claves para que las escrituras se repartan entre muchos dominios de CAS y particiones independientes. Un único hash sessions con un millón de campos martilleado por cada solicitud es un antipatrón; mil hashes sessions:{shard}, cada uno con su propio propietario y CAS, escalan de la forma en que lo hace el resto de Zaris. Ajusta el tamaño de la colección a la concurrencia de escritura y estos tipos se mantienen rápidos.

Cinco tipos de colección​

Los hashes, listas, conjuntos y conjuntos ordenados son cuatro de los cinco tipos de colección del lado del servidor que Zaris incluye. El quinto son los Streams: un registro de solo anexado nativo, replicado, con grupos de consumidores y lecturas bloqueantes, que tiene su propio artículo.

Si ya modelas datos en Redis, el modelo mental se traslada casi sin cambios: los tipos son los que conoces, los comandos son los que conoces, y la diferencia es lo que pasa cuando un nodo se cae. En Zaris, tu tabla de clasificación sigue ahí: se replicó, de la misma forma en que tus claves siempre lo han hecho.