325 votes

Chaînes Redis contre hachages Redis pour représenter JSON : efficacité ?

Je veux stocker une charge utile JSON dans redis. Il y a vraiment 2 façons de le faire :

  1. L'un d'entre eux utilise une simple chaîne de clés et de valeurs.
    key:user, value:payload (le blob JSON complet qui peut faire 100-200 KB)

    SET user:1 payload

  2. Utilisation des hachages

    HSET user:1 username "someone" HSET user:1 location "NY" HSET user:1 bio "STRING WITH OVER 100 lines"

Gardez à l'esprit que si j'utilise un hachage, la longueur de la valeur n'est pas prévisible. Elles ne sont pas toutes courtes comme dans l'exemple de la bio ci-dessus.

Lequel est le plus efficace en termes de mémoire ? L'utilisation de clés et de valeurs de type chaîne, ou l'utilisation d'un hachage ?

44 votes

Gardez également à l'esprit que vous ne pouvez pas (facilement) stocker un objet JSON imbriqué dans un ensemble de hachage.

4 votes

ReJSON peut également vous aider dans ce domaine : redislabs.com/blog/redis-as-a-json-store

465voto

BMiner Points 4471

Cet article peut fournir beaucoup d'informations à ce sujet : http://redis.io/topics/memory-optimization

Il y a plusieurs façons de stocker un tableau d'objets dans Redis ( spoiler : Je préfère l'option 1 pour la plupart des cas d'utilisation) :

  1. Stockez l'objet entier sous forme de chaîne codée en JSON dans une seule clé et gardez la trace de tous les objets à l'aide d'un ensemble (ou d'une liste, si cela est plus approprié). Par exemple :

    INCR id:users
    SET user:{id} '{"name":"Fred","age":25}'
    SADD users {id}

    D'une manière générale, c'est probablement la meilleure méthode dans la plupart des cas. Si l'objet comporte de nombreux champs, que vos objets ne sont pas imbriqués les uns dans les autres et que vous avez tendance à n'accéder qu'à un petit sous-ensemble de champs à la fois, il est préférable de choisir l'option 2.

    Avantages : considéré comme une "bonne pratique". Chaque objet est une clé Redis à part entière. L'analyse JSON est rapide, en particulier lorsque vous devez accéder à de nombreux champs de cet objet à la fois. Inconvénients : plus lent lorsque vous n'avez besoin d'accéder qu'à un seul champ.

  2. Stocker les propriétés de chaque objet dans un hash Redis.

    INCR id:users
    HMSET user:{id} name "Fred" age 25
    SADD users {id}

    Avantages : considéré comme une "bonne pratique". Chaque objet est une clé Redis à part entière. Il n'est pas nécessaire d'analyser les chaînes JSON. Inconvénients L'utilisation d'un objet peut être plus lente lorsque vous devez accéder à la totalité ou à la plupart des champs d'un objet. En outre, les objets imbriqués (objets dans des objets) ne peuvent pas être facilement stockés.

  3. Stocker chaque objet comme une chaîne JSON dans un hash Redis.

    INCR id:users
    HMSET users {id} '{"name":"Fred","age":25}'

    Cela vous permet de consolider un peu et de n'utiliser que deux clés au lieu de beaucoup de clés. L'inconvénient évident est que vous ne pouvez pas définir le TTL (et d'autres choses) sur chaque objet utilisateur, puisqu'il s'agit simplement d'un champ dans le hachage Redis et non d'une clé Redis à part entière.

    Avantages : L'analyse syntaxique JSON est rapide, surtout lorsque vous devez accéder à de nombreux champs de cet objet à la fois. Moins de "pollution" de l'espace de noms de la clé principale. Inconvénients : Environ la même utilisation de la mémoire que #1 lorsque vous avez beaucoup d'objets. Plus lent que #2 lorsque vous n'avez besoin d'accéder qu'à un seul champ. Probablement pas considéré comme une "bonne pratique".

  4. Stocker chaque propriété de chaque objet dans une clé dédiée.

    INCR id:users
    SET user:{id}:name "Fred"
    SET user:{id}:age 25
    SADD users {id}

    Selon l'article ci-dessus, cette option est presque jamais préférée (sauf si la propriété de l'objet doit avoir une valeur spécifique). TTL ou autre).

    Avantages : Les propriétés des objets sont des clés Redis à part entière, ce qui n'est pas forcément excessif pour votre application. Inconvénients : lent, utilise plus de mémoire, et n'est pas considéré comme une "meilleure pratique". Beaucoup de pollution de l'espace de noms de la clé principale.

Résumé général

L'option 4 n'est généralement pas préférée. Les options 1 et 2 sont très similaires, et toutes deux sont assez courantes. Je préfère l'option 1 (en général) parce qu'elle vous permet de stocker des objets plus complexes (avec plusieurs couches d'imbrication, etc.) L'option 3 est utilisée lorsque vous se soucier vraiment sur le fait de ne pas polluer l'espace de noms des clés principales (c'est-à-dire que vous ne voulez pas qu'il y ait beaucoup de clés dans votre base de données et que vous ne vous souciez pas de choses comme le TTL, le partage des clés, ou autre).

Si j'ai fait une erreur, merci de laisser un commentaire et de me laisser réviser la réponse avant de rétrograder. Merci ! :)

4 votes

Pour l'option 2, vous dites "éventuellement plus lent lorsque vous devez accéder à tous/la plupart des champs d'un objet". Cela a-t-il été testé ?

0 votes

@mikegreiling - J'ai soigneusement choisi les mots "probablement plus lent" parce que je n'ai pas fait de test :) Mais, ma théorie est que, dans la plupart des cas, si vous accédez à tous/la plupart des champs d'un objet, l'option 1 devrait être plus rapide que l'option 2, en particulier si le serveur Redis est un serveur distant.

5 votes

hmget est O(n) pour n champs obtenir avec l'option 1 serait toujours O(1). Théoriquement, oui, c'est plus rapide.

182voto

TheHippo Points 11900

Cela dépend de la façon dont vous accédez aux données :

Choisissez l'option 1 :

  • Si vous utilisez la plupart des champs sur la plupart de vos accès.
  • S'il y a une variance sur les clés possibles

Choisissez l'option 2 :

  • Si vous n'utilisez que des champs simples pour la plupart de vos accès.
  • Si vous savez toujours quels champs sont disponibles

P.S. : En règle générale, choisissez l'option qui nécessite le moins de requêtes dans la plupart de vos cas d'utilisation.

35 votes

L'option 1 n'est pas une bonne idée si modification simultanée de la JSON La charge utile est attendue (un problème classique de non-atomique read-modify-write ).

1 votes

Laquelle est la plus efficace parmi les options disponibles pour stocker le blob json comme une chaîne json ou comme un tableau d'octets dans Redis ?

10voto

Quelques compléments à un ensemble donné de réponses :

Tout d'abord, si vous voulez utiliser efficacement le hachage Redis, vous devez savoir que a le nombre maximal de clés et la taille maximale des valeurs - sinon, s'ils dépassent hash-max-ziplist-value ou hash-max-ziplist-entries, Redis les convertira en paires clé/valeur pratiquement habituelles sous un capot. ( voir hash-max-ziplist-value, hash-max-ziplist-entries ) Et la rupture sous un capot à partir d'une option de hachage EST TRÈS MAUVAISE, parce que chaque paire clé/valeur habituelle dans Redis utilise +90 bytes par paire.

Cela signifie que si vous commencez avec l'option deux et que vous sortez accidentellement de la valeur max-hash-ziplist, vous obtiendrez +90 octets pour CHAQUE ATTRIBUTE que vous avez dans le modèle utilisateur ! ( en fait pas +90 mais +70 voir la sortie console ci-dessous )

 # you need me-redis and awesome-print gems to run exact code
 redis = Redis.include(MeRedis).configure( hash_max_ziplist_value: 64, hash_max_ziplist_entries: 512 ).new 
  => #<Redis client v4.0.1 for redis://127.0.0.1:6379/0> 
 > redis.flushdb
  => "OK" 
 > ap redis.info(:memory)
    {
                "used_memory" => "529512",
          **"used_memory_human" => "517.10K"**,
            ....
    }
  => nil 
 # me_set( 't:i' ... ) same as hset( 't:i/512', i % 512 ... )    
 # txt is some english fictionary book around 56K length, 
 # so we just take some random 63-symbols string from it 
 > redis.pipelined{ 10000.times{ |i| redis.me_set( "t:#{i}", txt[rand(50000), 63] ) } }; :done
 => :done 
 > ap redis.info(:memory)
  {
               "used_memory" => "1251944",
         **"used_memory_human" => "1.19M"**, # ~ 72b per key/value
            .....
  }
  > redis.flushdb
  => "OK" 
  # setting **only one value** +1 byte per hash of 512 values equal to set them all +1 byte 
  > redis.pipelined{ 10000.times{ |i| redis.me_set( "t:#{i}", txt[rand(50000), i % 512 == 0 ? 65 : 63] ) } }; :done 
  > ap redis.info(:memory)
   {
               "used_memory" => "1876064",
         "used_memory_human" => "1.79M",   # ~ 134 bytes per pair  
          ....
   }
    redis.pipelined{ 10000.times{ |i| redis.set( "t:#{i}", txt[rand(50000), 65] ) } };
    ap redis.info(:memory)
    {
             "used_memory" => "2262312",
          "used_memory_human" => "2.16M", #~155 byte per pair i.e. +90 bytes    
           ....
    }

Pour la réponse de TheHippo, les commentaires sur la première option sont trompeurs :

hgetall/hmset/hmget à la rescousse si vous avez besoin de tous les champs ou de plusieurs opérations get/set.

Pour la réponse de BMiner.

La troisième option est en fait très amusante, pour les ensembles de données avec max(id) < has-max-ziplist-value cette solution a une complexité O(N), parce que, surprise, Reddis stocke les petits hashs comme un conteneur de type tableau d'objets longueur/clé/valeur !

Mais souvent, les hachages ne contiennent que quelques champs. Lorsque les hachages sont petits, nous pouvons simplement les encoder dans une structure de données O(N), comme un tableau linéaire avec des paires clé-valeur préfixées par la longueur. Puisque nous ne faisons cela que lorsque N est petit, le temps amorti pour les commandes HGET et HSET est toujours O(1) : le hachage sera converti en une véritable table de hachage dès que le nombre d'éléments qu'il contient augmentera trop.

Mais ne vous inquiétez pas, vous casserez hash-max-ziplist-entries très rapidement et voilà, vous êtes maintenant à la solution numéro 1.

La deuxième option sera très probablement la quatrième solution sous un capot car comme le dit la question :

Gardez à l'esprit que si j'utilise un hachage, la longueur de la valeur n'est pas prévisible. Elles ne sont pas toutes courtes comme dans l'exemple de la bio ci-dessus.

Et comme vous l'avez déjà dit : la quatrième solution est la plus chère +70 octets pour chaque attribut, c'est certain.

Ma suggestion : comment optimiser un tel ensemble de données ?

Vous avez deux options :

  1. Si vous ne pouvez pas garantir la taille maximale de certains attributs de l'utilisateur, alors vous optez pour la première solution, et si la question de la mémoire est cruciale, alors compressez le json de l'utilisateur avant de le stocker dans redis.

  2. Si vous pouvez forcer la taille maximale de tous les attributs. Alors vous pouvez définir hash-max-ziplist-entries/value et utiliser les hashs soit comme un hash par représentation de l'utilisateur OU comme optimisation de la mémoire des hashs à partir de ce sujet d'un guide Redis : https://redis.io/topics/memory-optimization et stocker l'utilisateur sous forme de chaîne json. Dans les deux cas, vous pouvez également compresser les attributs longs de l'utilisateur.

1voto

Ali Alp Points 305

Nous avons eu un problème similaire dans notre environnement de production, nous avons eu l'idée de gzipper la charge utile si elle dépasse un certain seuil de KB.

J'ai un dépôt uniquement dédié à cette librairie client Redis. ici

L'idée de base est de détecter la charge utile si sa taille est supérieure à un certain seuil, puis de la compresser et de la décompresser en base-64, puis de conserver la chaîne compressée comme une chaîne normale dans le redis. Lors de la récupération, il faut détecter si la chaîne est une chaîne valide en base-64 et si c'est le cas, la décompresser.

l'ensemble de la compression et de la décompression sera transparent et vous gagnerez près de 50% de trafic réseau.

Résultats de l'évaluation comparative de la compression

BenchmarkDotNet=v0.12.1, OS=macOS 11.3 (20E232) [Darwin 20.4.0]
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.201
  [Host] : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT DEBUG

Méthode

Moyenne

Erreur

Ecart-type

Gen 0

Gen 1

Gen 2

Alloué

WithCompressionBenchmark

668,2 ms

13,34 ms

27,24 ms

-

-

-

4.88 MB

WithoutCompressionBenchmark

1 387,1 ms

26,92 ms

37,74 ms

-

-

-

2.39 MB

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X