web-dev-qa-db-fra.com

Redis strings vs Redis hashes pour représenter JSON: efficacité?

Je souhaite stocker une charge JSON dans Redis. Il y a vraiment 2 façons de faire ça:

  1. Une en utilisant une simple chaîne de clés et de valeurs.
    clé: utilisateur, valeur: charge utile (la totalité du blob JSON pouvant aller de 100 à 200 Ko)

    SET user:1 payload

  2. Utiliser des hashes

    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. Ils ne sont pas tous courts comme l'exemple de la bio ci-dessus.

Quel est le plus efficace en mémoire? Vous utilisez des clés de chaîne et des valeurs ou utilisez un hachage?

253
Henley Chiu

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

Optez pour l'option 1:

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

Optez pour l'option 2:

  • Si vous utilisez uniquement des champs uniques sur la plupart de vos accès.
  • Si vous savez toujours quels champs sont disponibles

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

146
TheHippo

Cet article peut fournir beaucoup d’idées ici: http://redis.io/topics/memory-optimization

Il y a plusieurs façons de stocker un tableau d'objets dans Redis ( spoiler : J'aime l'option 1 dans la plupart des cas d'utilisation):

  1. Stockez l'intégralité de l'objet sous forme de chaîne codée JSON dans une clé unique et suivez tous les objets à l'aide d'un jeu (ou d'une liste, si nécessaire). Par exemple:

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

    De manière générale, c'est probablement la meilleure méthode dans la plupart des cas. S'il y a beaucoup de champs dans l'objet, que vos objets ne sont pas imbriqués avec d'autres objets et que vous avez tendance à accéder à un petit sous-ensemble de champs à la fois, il peut être préférable d'utiliser 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 à plusieurs champs à la fois pour cet objet. Inconvénients : plus lent lorsque vous ne devez accéder qu'à un seul champ.

  2. Stocke les propriétés de chaque objet dans un hachage 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. Pas besoin d'analyser les chaînes JSON. Inconvénients : éventuellement plus lent lorsque vous devez accéder à tous/à la plupart des champs d'un objet. De plus, les objets imbriqués (objets dans les objets) ne peuvent pas être facilement stockés.

  3. Stockez chaque objet en tant que chaîne JSON dans un hachage Redis.

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

    Cela vous permet de consolider un peu et d'utiliser seulement 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 éléments) sur chaque objet utilisateur, car il s'agit simplement d'un champ dans le hachage Redis et non d'une clé Redis complète.

    Avantages : l'analyse JSON est rapide, en particulier lorsque vous devez accéder à plusieurs champs à la fois pour cet objet. Moins "polluant" de l'espace de noms de la clé principale. Inconvénients : À peu près la même utilisation de la mémoire que # 1 lorsque vous avez beaucoup d'objets. Plus lent que le n ° 2 lorsque vous ne devez accéder qu'à un seul champ. Probablement pas considéré comme une "bonne pratique".

  4. Stockez 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 (à moins que la propriété de l'Object ne soit spécifique TTL ou autre).

    Avantages : les propriétés de l'objet sont des clés Redis complètes, qui pourraient ne pas être excessives pour votre application. Inconvénients : lent, utilise plus de mémoire et n'est pas considéré comme une "pratique exemplaire". Beaucoup de pollution de l'espace de noms de la clé principale.

Sommaire 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 sont très communes. Je préfère l'option 1 (en général) car elle vous permet de stocker des objets plus complexes (avec plusieurs couches d'imbrication, etc.). L'option 3 est utilisée lorsque soigne pour ne pas polluer l'espace de noms principal (c.-à-d. 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 la durée de vie, le partage de clé ou autre).

Si quelque chose ne va pas ici, veuillez laisser un commentaire et me permettre de réviser la réponse avant de voter. Merci! :)

377
BMiner

Quelques ajouts à un ensemble de réponses donné:

Tout d’abord, si vous voulez utiliser efficacement le hachage Redis, vous devez connaître le nombre maximal de clés et leur valeur, sinon leur valeur maximale - sinon, si elles sortent des valeurs hash-max-ziplist-value ou hash-max-ziplist-entrées, Redis le convertira de manière pratique. paires clé/valeur habituelles sous un capot. (voir hash-max-ziplist-value, hash-max-ziplist-entrées) Et rompre sous le capot des options de hachage IS REALY BAD, car chaque paire clé/valeur habituelle dans Redis utilise +90 octets par paire.

Cela signifie que si vous démarrez avec l'option deux et que vous rompez accidentellement la valeur max-hash-ziplist, vous obtiendrez +90 octets par CHAQUE ATTRIBUT que vous avez dans le modèle utilisateur! (en réalité pas le +90 mais le +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 BMiner répondre.

La troisième option est vraiment amusante, pour les données avec max (id) <has-max-ziplist-value, cette solution a la complexité O(N), car, surprise, Reddis stocke de petits hachages en tant que conteneur de type tableau d'objets longueur/clé/valeur!

Mais souvent, les hashes 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 de valeurs de clé avec un préfixe de longueur. Puisque nous ne faisons cela que lorsque N est petit, le temps amorti pour les commandes HGET et HSET est toujours égal à O (1): le hachage sera converti en une table de hachage réelle dès que le nombre d'éléments qu'il contient augmentera trop.

Mais ne vous inquiétez pas, vous supprimez très rapidement les entrées hash-max-ziplist et vous avez atteint la solution numéro 1.

La deuxième option ira 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. Ils ne sont pas tous courts comme l'exemple de la bio ci-dessus.

Et comme vous l'avez déjà dit: la quatrième solution est certainement le plus coûteux de 70 octets par attribut.

Ma suggestion pour optimiser cet ensemble de données:

Vous avez deux options:

  1. Si vous ne pouvez pas garantir la taille maximale de certains attributs d’utilisateur, choisissez la première solution et si l’importance de la mémoire est cruciale, compressez l’utilisateur json avant de le stocker dans redis.

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