Aller au contenu principal

Des structures de données qui se répliquent : hashes, listes, sets et sorted sets dans Zaris

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

Hashes, listes, sets et sorted sets natifs, répliqués comme toute autre clé dans Zaris

Zaris 1.x stockait des valeurs. Zaris 2.0.0 stocke des collections. Les hashes, listes, sets et sorted sets sont désormais des types de premier ordre, côté serveur, assortis de véritables API .NET typées — et, tout aussi important, ils se comportent comme tout le reste dans le store.

C'est cette dernière partie que l'on se représente généralement de travers. Dans beaucoup de systèmes, les « structures de données » sont une illusion côté client : une bibliothèque sérialise un dictionnaire en un blob, le Put, et appelle cela un hash. Cela fonctionne jusqu'à ce que deux clients touchent la même collection en même temps, ou jusqu'à ce que vous demandiez ce qui se passe lors d'un basculement. Dans Zaris, un hash est un hash sur le serveur. Il réside sur la partition propriétaire de sa clé, il se réplique vers le réplica de cette partition, et il survit à la perte d'un nœud exactement comme le fait une simple clé.

Quatre types, une seule API typée​

Chaque type de collection découle de client.DataStructures, indexé par le nom que vous utiliseriez pour n'importe quelle autre clé :

// 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();

Rien ici n'est un utilitaire qui récupère un blob et le mute dans votre processus. Hash("user:42").SetAsync(...) est une opération côté serveur sur la collection nommée user:42, de la même façon que Put("user:42", ...) opère sur la valeur nommée user:42.

Même placement, même réplication, même basculement​

Voici la décision de conception qui rend ces types dignes de confiance : une opération sur une structure de données emprunte exactement le même chemin répliqué qu'une écriture de clé ordinaire.

Chaque collection est une unique valeur logique qui réside sur la partition propriétaire de sa clé. Elle est placée par la même carte de partitions, couverte par le même facteur de réplication, et bascule de la même manière. Il n'y a pas de « sous-système de collections » distinct doté de sa propre logique de durabilité — si vous comprenez comment une clé Zaris est placée et répliquée, vous comprenez déjà comment l'est un sorted set.

En interne, une opération de mutation est une lecture-modification-écriture par compare-and-set (CAS) sur la valeur entière sur le même chemin répliqué Put/Get qu'utilisent les clés ordinaires. Ajouter un membre à un sorted set lit la collection courante, applique le changement, et le valide avec un compare-and-set ; si un autre écrivain est arrivé le premier, le CAS échoue et l'opération réessaie sur la valeur fraîche. Parce que la validation est le même Put répliqué, la mise à jour est :

  • Atomique par clé — une mutation de collection unique atterrit entièrement ou pas du tout.
  • Répliquée vers le réplica de la partition — le changement est durable face à la perte de nœud que Zaris est conçu pour surmonter.
  • Cohérente avec les clés ordinaires — une mise à jour de hash et une mise à jour de clé obtiennent les mêmes garanties, parce qu'elles sont la même écriture.

C'est toute l'astuce : au lieu d'un modèle de cohérence sur mesure par type de donnée, Zaris exprime chaque mutation en termes de l'unique opération qu'il réplique déjà correctement.

Les mêmes collections, depuis n'importe quel client Redis​

Chacun de ces types est également exposé via le front-end protocole-Redis (RESP), de sorte qu'un client Redis dans n'importe quel langage opère sur les mêmes collections côté serveur — et non sur un ensemble parallèle résidant ailleurs.

# 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 service .NET écrivant via client.DataStructures.Hash("user:42") et un service Python émettant HSET user:42 touchent le même hash sur la même partition. La couche RESP traduit la commande vers la collection native ; ce n'est pas une seconde implémentation.

Nous l'avons validé à travers les langages plutôt que de simplement l'affirmer : les suites de clients multilangages affichent 38/38 au vert en .NET, 51/51 en Java et 40/40 en Ruby, exerçant la même sémantique de collection à travers le client de chaque langage.

L'arbitrage honnête : le CAS sur valeur entière​

Le CAS sur valeur entière est ce qui achète la garantie propre, et c'est aussi là que réside le coût. Chaque mutation lit et réécrit la valeur entière de la collection, et les écrivains concurrents sur la même collection entrent en contention sur le compare-and-set — un perdant réessaie sur la nouvelle valeur.

En pratique, ces types conviennent parfaitement aux collections de petite à moyenne taille, où réécrire la valeur entière est peu coûteux, et aux profils à forte lecture ou à écriture modérée, où les réessais de CAS sont rares.

Là où vous le sentiriez, c'est à l'extrême opposé : une très grosse collection avec de nombreux écrivains concurrents très chauds. Un hash gigantesque que des dizaines de clients martèlent en même temps passera un temps réel à perdre et réessayer des compare-and-set, et chaque écriture paie pour sérialiser la valeur entière.

Le remède est le geste habituel des systèmes distribués — répartir l'énorme collection sur plusieurs clés afin que les écritures se diffusent sur de nombreux domaines de CAS et partitions indépendants. Un unique hash sessions à un million de champs martelé par chaque requête est un anti-pattern ; un millier de hashes sessions:{shard}, chacun avec son propre propriétaire et son propre CAS, monte en charge comme le fait le reste de Zaris. Adaptez la taille de la collection à la concurrence d'écriture et ces types restent rapides.

Cinq types de collection​

Les hashes, listes, sets et sorted sets sont quatre des cinq types de collection côté serveur que Zaris propose. Le cinquième est Streams — un journal natif, répliqué, en ajout seul, doté de groupes de consommateurs et de lectures bloquantes — qui bénéficie de son propre article.

Si vous modélisez déjà des données dans Redis, le modèle mental se reporte presque inchangé : les types sont ceux que vous connaissez, les commandes sont celles que vous connaissez, et la différence réside dans ce qui se passe quand un nœud tombe. Dans Zaris, votre leaderboard est toujours là — il s'est répliqué, de la même manière que vos clés l'ont toujours fait.