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

287

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

  1. Un utilisant une simple chaîne de clés et de valeurs.
    clé: utilisateur, valeur: charge utile (l'intégralité de l'objet blob JSON qui peut être compris entre 100 et 200 Ko)

    SET user:1 payload

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

Quelle est la mémoire la plus efficace? À l'aide de clés et de valeurs de chaîne, ou à l'aide d'un hachage?

Henley Chiu
la source
37
Gardez également à l'esprit que vous ne pouvez pas (facilement) stocker un objet JSON imbriqué dans un ensemble de hachage.
Jonatan Hedborg
3
ReJSON peut également vous aider ici: redislabs.com/blog/redis-as-a-json-store
Cihan B.
2
quelqu'un at-il utilisé ReJSON ici?
Swamy

Réponses:

168

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.
  • En cas de divergence sur les clés possibles

Optez pour l'option 2:

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

PS: 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.

TheHippo
la source
28
L'option 1 n'est pas une bonne idée si une modification simultanée de la JSONcharge utile est attendue (un problème classique de non-atomique read-modify-write ).
Samveen
1
Quelle est la plus efficace parmi les options disponibles de stockage de l'objet blob json sous forme de chaîne json ou de tableau d'octets dans Redis?
Vinit89
422

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

Il existe de nombreuses façons de stocker un tableau d'objets dans Redis ( spoiler : j'aime l'option 1 pour la plupart des cas d'utilisation):

  1. Stockez l'intégralité de l'objet sous forme de chaîne codée JSON dans une seule clé et conservez une trace de tous les objets à l'aide d'un ensemble (ou d'une liste, le cas échéant). 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. S'il y a beaucoup de champs dans l'objet, vos objets ne sont pas imbriqués avec d'autres objets, et vous avez tendance à accéder uniquement à un petit sous-ensemble de champs à la fois, il peut être préférable d'opter pour 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 simultanément à de nombreux champs pour cet objet. Inconvénients : plus lent lorsque vous n'avez besoin d'accéder qu'à un seul champ.

  2. Stockez 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 : peut-être 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 sous forme de 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 uniquement 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 pour cet objet à la fois. Moins "polluant" de l'espace de noms principal. Inconvénients : À peu près 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. 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 n'est presque jamais préférée (sauf si la propriété de l'objet doit avoir un TTL spécifique ou quelque chose).

    Avantages : Les propriétés des objets sont des clés Redis à part entière, qui ne sont peut-être pas excessives 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 principal.

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 elles sont toutes les deux assez courantes. Je préfère l'option 1 (en général) car elle vous permet de stocker des objets plus compliqués (avec plusieurs couches d'imbrication, etc.) L'option 3 est utilisée lorsque vous vous souciez vraiment de ne pas polluer l'espace de noms des clés principales (c'est-à-dire que vous ne voulez pas là pour avoir beaucoup de clés dans votre base de données et vous ne vous souciez pas de choses comme TTL, partage de clés, ou autre).

Si je me trompe, pensez à laisser un commentaire et à me permettre de réviser la réponse avant de voter. Merci! :)

BMiner
la source
4
Pour l'option n ° 2, vous dites "peut-être plus lent lorsque vous devez accéder à tous / la plupart des champs d'un objet". Cela a-t-il été testé?
mikegreiling
4
hmget est O (n) pour n champs get avec l'option 1 serait toujours O (1). Théoriquement, oui, c'est plus rapide.
Aruna Herath
4
Que diriez-vous de combiner les options 1 et 2 avec un hachage? Utiliser l'option 1 pour les données rarement mises à jour et l'option 2 pour les données fréquemment mises à jour? Disons, nous stockons des articles et nous stockons des champs comme le titre, l'auteur et l'URL dans une chaîne JSON avec une clé générique comme objet stockons des champs comme les vues, les votes et les votants avec des clés séparées? De cette façon, avec une seule requête READ, vous obtenez l'intégralité de l'objet et pouvez toujours mettre à jour rapidement des parties dynamiques de votre objet? Les mises à jour relativement peu fréquentes des champs de la chaîne JSON peuvent être effectuées en lisant et en réécrivant l'intégralité de l'objet dans une transaction.
arun
2
Selon cela: ( instagram-engineering.tumblr.com/post/12202313862/… ), il est recommandé de stocker dans plusieurs hachages en termes de consommation de mémoire. Donc, après l'optimisation d'Arun, nous pouvons faire: 1- créer plusieurs hachages stockant la charge utile json sous forme de chaînes pour les données rarement mises à jour, et 2- créer plusieurs hachages stockant les champs json pour les données fréquemment mises à jour
Aboelnour
2
En cas d'option1, pourquoi l'ajoutons-nous à un ensemble? Pourquoi ne pouvons-nous pas simplement utiliser la commande Get et vérifier si le retour n'est pas nul.
Pragmatique
8

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

Tout d'abord, si vous utilisez efficacement le hachage Redis, vous devez connaître le nombre de clés et la valeur maximale de la taille des clés - sinon, si elles décomposent la valeur de hachage-max-ziplist ou les entrées de hachage-max-ziplist, Redis le convertira en pratiquement paires de clés / valeurs habituelles sous un capot. (voir hash-max-ziplist-value, hash-max-ziplist-entries) Et briser sous un capot à partir d'une option de hachage EST VRAIMENT MAUVAIS, car chaque paire clé / valeur habituelle à l'intérieur de Redis utilise +90 octets par paire.

Cela signifie que si vous commencez avec l'option deux et sortez accidentellement de la valeur max-hash-ziplist, vous obtiendrez +90 octets par CHAQUE ATTRIBUT que vous avez dans le modèle utilisateur! (en fait 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 la réponse de BMiner.

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

Mais souvent, les hachages ne contiennent que quelques champs. Lorsque les hachages sont petits, nous pouvons simplement les coder dans une structure de données O (N), comme un tableau linéaire avec des paires de valeurs de clé préfixées par la longueur. Puisque nous ne le faisons 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 très rapidement les entrées de hachage-max-ziplist et vous voilà maintenant à la solution numéro 1.

La deuxième option ira très probablement à la quatrième solution sous un capot, car comme l'indique 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 bio ci-dessus.

Et comme vous l'avez déjà dit: la quatrième solution est certainement le +70 octet le plus cher pour chaque attribut.

Ma suggestion pour optimiser un tel ensemble de données:

Vous avez deux options:

  1. Si vous ne pouvez pas garantir la taille maximale de certains attributs utilisateur, optez pour la première solution et si la question 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. Ensuite, vous pouvez définir des entrées / valeur de hachage-max-ziplist et utiliser des hachages soit comme un hachage par représentation utilisateur OU comme optimisation de la mémoire de hachage à partir de cette rubrique d'un guide Redis: https://redis.io/topics/memory-optimization et stocker l'utilisateur sous forme de chaîne json. Dans tous les cas, vous pouvez également compresser de longs attributs utilisateur.

Алексей Лещук
la source