web-dev-qa-db-ja.com

Redisの文字列とRedisはJSONを表すためにハッシュします。

JSONペイロードをredisに保存したいです。これには2つの方法があります。

  1. 単純な文字列のキーと値を使用するもの。
    キー:ユーザー、値:ペイロード(JSON BLOB全体、100 - 200 KB)

    SET user:1 payload

  2. ハッシュを使う

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

ハッシュを使用した場合、値の長さは予測できません。上記のbioの例のように、それらすべてが短いわけではありません。

どちらがよりメモリ効率的ですか?文字列のキーと値を使うのか、それともハッシュを使うのか?

244
Henley Chiu

データへのアクセス方法によって異なります。

オプション1に進みます。

  • ほとんどのアクセスでほとんどのフィールドを使用している場合。
  • 可能なキーに差異がある場合

オプション2に進みます。

  • あなたがあなたのアクセスのほとんどでただ一つのフィールドを使うならば。
  • どのフィールドが利用可能かを常に知っている場合

P.S .:経験則として、ほとんどのユースケースでクエリの数が少ないオプションを選択してください。

140
TheHippo

この記事はここで多くの洞察を提供することができます: http://redis.io/topics/memory-optimization

オブジェクトの配列をRedis(スポイラーに保存するには多くの方法があります:ほとんどのユースケースではオプション1が好きです):

  1. オブジェクト全体をJSONエンコードされた文字列として単一のキーに格納し、セット(より適切な場合はリスト)を使用してすべてのオブジェクトを追跡します。例えば:

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

    一般的に言って、これはたいていの場合おそらく最善の方法です。オブジェクトにたくさんのフィールドがある場合、あなたのオブジェクトは他のオブジェクトと入れ子になっていません、そしてあなたは一度に少数のフィールドのサブセットだけにアクセスする傾向があるので、オプション2を使うのが良いかもしれません。

    利点:「良い習慣」と見なされます。各オブジェクトは本格的なRedisキーです。 JSONの解析は高速です。特に、このObjectの多数のフィールドに同時にアクセスする必要がある場合は特にそうです。 デメリット:単一のフィールドにアクセスする必要がある場合は遅くなります。

  2. 各オブジェクトのプロパティをRedisハッシュに格納します。

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

    利点:「良い習慣」と見なされます。各オブジェクトは本格的なRedisキーです。 JSON文字列を解析する必要はありません。 デメリット:Object内の全ての/ほとんどのフィールドにアクセスする必要がある場合はおそらく遅くなります。また、ネストしたオブジェクト(オブジェクト内のオブジェクト)は簡単に格納できません。

  3. 各オブジェクトをRedisハッシュのJSON文字列として格納します。

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

    これにより、少し統合して、たくさんのキーの代わりに2つのキーだけを使用することができます。明らかに不利な点は、各ユーザーオブジェクトにTTL(およびその他のもの)を設定できないことです。これはRedisハッシュ内の単なるフィールドであり、本格的なRedisキーではないからです。

    利点:JSONの解析は高速です。特に、このオブジェクトの多数のフィールドに一度にアクセスする必要がある場合は特にそうです。主キー名前空間の「汚染」が少ない。 デメリット:たくさんのオブジェクトがあるとき#1とほぼ同じメモリ使用量。あなたがただ一つのフィールドにアクセスする必要があるとき#2より遅い。おそらく「良い習慣」とは見なされません。

  4. 各オブジェクトの各プロパティを専用のキーに格納します。

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

    上記の記事によると、このオプションはほとんどありませんpreferred(オブジェクトのプロパティに特定の TTL などを指定する必要がない限り) 。

    利点:オブジェクトのプロパティは、本格的なRedisキーです。 デメリット:遅く、より多くのメモリを使用します。「ベストプラクティス」とは見なされません。主キー名前空間の汚染がたくさん。

全体的なまとめ

選択肢4は一般的には好ましくない。オプション1と2はよく似ていますが、どちらもかなり一般的です。 (一般的に言って)オプション1は、複雑なオブジェクト(複数のネスティング層などを含む)を格納できるため、オプション1をお勧めします。really careabout主キーの名前空間を汚染しないようにします(つまり、データベースに多くのキーが存在することを望まず、TTL、キーの分割、その他のことを気にする必要はありません)。

ここで何か問題があった場合は、コメントを残して投票の前に回答を修正できるようにしてください。ありがとうございます。 :)

367
BMiner

与えられた答えのセットへのいくつかの追加:

まず最初にRedisハッシュを効率的に使うつもりならば、あなたはキーカウントの最大数と最大サイズの値を知っていなければなりません。フードの下の通常のキー/値ペア。 (hash-max-ziplist-value、hash-max-ziplist-entriesを参照)そしてハッシュオプションIS REALY BADからの抜け道は、Redis内の通常の各キー/値ペアは+ 90バイトを使用するためペアあたり。

それはあなたがオプション2から始めて、誤ってmax-hash-ziplist-valueから抜け出したなら、あなたはあなたがユーザーモデルの中に持っているそれぞれの属性ごとに+ 90バイトを得ることを意味します! (実際には+ 90ではなく+ 70です。下記のコンソール出力を参照してください)

 # 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    
           ....
    }

TheHippoの回答では、オプション1に関するコメントは誤解を招く可能性があります。

あなたがすべてのフィールドまたは複数のget/set操作を必要とするならば、hgetall/hmset/hmgetは救助に行きます。

BMinerのために答えます。

Max(id)<has-max-ziplist-valueのデータセットでは、この解決法はO(N)の複雑さを持ちます。驚いたことに、Reddisは配列のような小さなハッシュを格納します。長さ/キー/値オブジェクト

しかし多くの場合、ハッシュはほんの少しのフィールドを含みます。ハッシュが小さい場合は、代わりにO(N)データ構造でエンコードすることができます(長さプレフィックスのキーと値のペアを持つ線形配列のように)。 Nが小さいときだけこれをするので、HGETとHSETコマンドのための償却時間はまだO(1)です:それが含む要素の数が増え過ぎるとすぐにハッシュは本当のハッシュテーブルに変換されるでしょう

しかし、心配する必要はありません。hash-max-ziplist-entriesをすばやく壊すことができます。実際には、解決策1になっています。

質問が述べているように、2番目の選択肢は、おそらくフードの下で4番目の解決策に進むでしょう。

ハッシュを使用した場合、値の長さは予測できません。上記のbioの例のように、それらすべてが短いわけではありません。

そしてあなたがすでに言ったように:第四の解決策は確かに各属性ごとに最も高価な+70バイトです。

そのようなデータセットを最適化する方法を私の提案:

2つの選択肢があります。

  1. いくつかのユーザー属性の最大サイズを最初の解決策よりも保証できない場合、およびメモリーの問題が重大な場合は、ユーザーjsonを圧縮してからredisに格納してください。

  2. すべての属性の最大サイズを強制できる場合あなたはhash-max-ziplist-entries/valueを設定でき、Redisガイドのこのトピックからハッシュメモリ最適化としてユーザ表現ORごとに一つのハッシュとしてハッシュを使うことができます: https:// redis.io/topics/memory-optimization ユーザーをjson文字列として保存します。どちらの方法でも、長いユーザー属性も圧縮できます。