首页 文章

Redis字符串与Redis哈希表示JSON:效率?

提问于
浏览
226

我想将JSON有效负载存储到redis中 . 我有两种方法可以做到这一点:

  • 使用简单的字符串键和值 .
    key:user,value:payload(整个JSON blob,可以是100-200 KB)

SET user:1 payload

  • 使用哈希

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

请记住,如果我使用哈希值,则值的长度是不可预测的 . 它们并非都是短的,例如上面的生物例子 .

哪个内存更有效?使用字符串键和值,还是使用哈希?

3 回答

  • 346

    这取决于您访问数据的方式:

    选择方案1:

    • 如果您在大多数访问中使用大多数字段 .

    • 如果可能的键有差异

    选择2:

    • 如果您在大多数访问中仅使用单个字段 .

    • 如果您始终知道哪些字段可用

    P.S . :作为一个经验法则,请选择在大多数用例中需要较少查询的选项 .

  • 130

    这篇文章可以提供很多见解:http://redis.io/topics/memory-optimization

    有很多方法可以在Redis中存储一个对象数组( spoiler :我喜欢大多数用例的选项1):

    • 将整个对象作为JSON编码的字符串存储在单个键中,并使用集合(或列表,如果更合适)跟踪所有对象 . 例如:
    INCR id:users
    SET user:{id} '{"name":"Fred","age":25}'
    SADD users {id}
    

    一般来说,这可能是大多数情况下最好的方法 . 如果对象中有很多字段,则对象不会与其他对象嵌套,并且您一次只能访问一小部分字段,最好选择选项2 .

    Advantages :被认为是"good practice."每个对象都是一个完整的Redis密钥 . JSON解析很快,特别是当您需要同时访问此Object的许多字段时 . Disadvantages :只需访问单个字段时速度较慢 .

    • 将每个Object的属性存储在Redis哈希中 .
    INCR id:users
    HMSET user:{id} name "Fred" age 25
    SADD users {id}
    

    Advantages :被认为是"good practice."每个对象都是一个完整的Redis密钥 . 无需解析JSON字符串 . Disadvantages :当您需要访问Object中的所有/大多数字段时可能会更慢 . 此外,无法轻松存储嵌套对象(对象内的对象) .

    • 将每个Object存储为Redis哈希中的JSON字符串 .
    INCR id:users
    HMSET users {id} '{"name":"Fred","age":25}'
    

    这允许您合并一点,只使用两个键而不是许多键 . 明显的缺点是你不能在每个用户对象上设置TTL(和其他东西),因为它只是Redis哈希中的一个字段而不是一个完整的Redis密钥 .

    Advantages :JSON解析速度很快,尤其是当您需要同时访问此Object的许多字段时 . 较少"polluting"的主键名称空间 . Disadvantages :当你有很多对象时,大约与#1相同的内存使用量 . 当您只需要访问单个字段时,比#2慢 . 可能不被认为是"good practice."

    • 将每个Object的每个属性存储在专用密钥中 .
    INCR id:users
    SET user:{id}:name "Fred"
    SET user:{id}:age 25
    SADD users {id}
    

    根据上面的文章,这个选项几乎从不首选(除非Object的属性需要特定的TTL或其他东西) .

    Advantages :对象属性是完整的Redis密钥,对您的应用程序而言可能不会过度 . Disadvantages :慢,使用更多内存,而不考虑"best practice."主键名称空间有很多污染 .

    总结

    方案4通常不是优选的 . 选项1和2非常相似,它们都很常见 . 我更喜欢选项1(一般来说),因为它允许您存储更复杂的对象(具有多层嵌套等) . 当您真正关心不污染主键名称空间时使用选项3(即您不关心像TTL,关键字分片等等 .

    如果我在这里遇到问题,请考虑发表评论并允许我在下注之前修改答案 . 谢谢! :)

  • 2

    对一组给定答案的一些补充:

    首先,如果你要有效地使用Redis散列,你必须知道密钥数最大数和最大值 - 否则如果它们突破hash-max-ziplist-value或hash-max-ziplist-entries Redis会将它转换为实际引擎盖下的常用键/值对 . (请参阅hash-max-ziplist-value,hash-max-ziplist-entries)并且从哈希选项中打破引擎是非常糟糕的,因为Redis中的每个常用键/值对每对使用90个字节 .

    这意味着如果你从选项2开始并意外地突破max-hash-ziplist-value,你将获得每个EACH ATTRIBUTE 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的回答,对备选方案一的评论具有误导性:

    如果你需要所有字段或多个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号 .

    第二个选项很可能会转到引擎盖下的第四个解决方案,因为问题表明:

    请记住,如果我使用哈希值,则值长度不可预测 . 它们并非都是短的,例如上面的生物例子 .

    正如您已经说过的那样:第四种解决方案是每个属性最昂贵的70字节 .

    我建议如何优化这样的数据集:

    你有两个选择:

    • 如果你不能保证某些用户属性的最大大小比第一个解决方案更大,并且内存问题比在redis中存储之前压缩用户json至关重要 .

    • 如果可以强制所有属性的最大大小 . 您可以设置hash-max-ziplist-entries / value,并将哈希值用作每个用户表示的一个哈希值或从Redis指南的主题中使用哈希内存优化:https://redis.io/topics/memory-optimization并将用户存储为json字符串 . 无论哪种方式,您还可以压缩长用户属性 .

相关问题