Redis字符串与Redis散列代表JSON:效率?


287

我想将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示例。

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


37
另外请记住,您不能(轻松)将嵌套的JSON对象存储在哈希集中。
乔纳丹·赫德堡

3
ReJSON也可以在这里提供帮助:redislabs.com/blog/redis-as-a-json-store
Cihan B.

2
有人在这里使用ReJSON吗?
斯瓦米

Answers:


168

这取决于您如何访问数据:

选择选项1:

  • 如果您在大多数访问权限中使用大多数字段。
  • 如果可能的键存在差异

选择选项2:

  • 如果您在大多数访问中仅使用单个字段。
  • 如果您始终知道哪些字段可用

PS:根据经验,请选择在大多数用例中只需要较少查询的选项。


28
选项1不是如果一个好主意并发修改所述的JSON有效载荷预期(的典型问题非原子 read-modify-write)。
Samveen '16

1
在将json blob存储为Redis中的json字符串或字节数组的可用选项中,哪一种效率更高?
Vinit89 '18

422

本文可以在此处提供很多见解: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字符串。 缺点:当您需要访问对象中的所有/大多数字段时,速度可能会变慢。同样,嵌套对象(对象内的对象)也无法轻松存储。

  3. 将每个对象作为JSON字符串存储在Redis哈希中。

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

    这使您可以进行合并,并且仅使用两个键,而不是很多键。明显的缺点是您不能在每个用户对象上设置TTL(及其他内容),因为它只是Redis哈希中的一个字段,而不是成熟的Redis密钥。

    优点:JSON解析速度很快,尤其是当您需要一次访问此Object的多个字段时。减少主键名称空间的“污染”。 缺点:当您有很多对象时,内存使用率与#1差不多。当您只需要访问一个字段时,速度比#2慢。可能不被视为“良好做法”。

  4. 将每个对象的每个属性存储在专用键中。

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

    根据上面的文章,几乎永远不会选择此选项(除非Object的属性需要具有特定的TTL或其他内容)。

    优点:对象属性是成熟的Redis密钥,对于您的应用程序来说可能并不算过大。 缺点:速度慢,占用更多内存,并且不被视为“最佳实践”。主键名称空间受到很多污染。

总体总结

选项4通常不是首选。选项1和2非常相似,而且都很常见。我更喜欢选项1(通常来说),因为它允许您存储更复杂的对象(具有多层嵌套等)。当您真正关心不污染主键名称空间(即您不希望在那里)时,可以使用选项3。成为数据库中的很多键,并且您无需担心TTL,键分片等问题。

如果我在这里遇到问题,请考虑发表评论,并允许我修改答案,然后再投票。谢谢!:)


4
对于选项2,您说“当您需要访问对象中的所有/大多数字段时,可能会变慢”。已经测试过了吗?
mikegreiling

4
hmget为O(n)为ñ领域获得与选项1仍然是O(1)。从理论上讲,是的,它速度更快。
Aruna Herath,2015年

4
如何将选项1和2与哈希结合在一起?对不经常更新的数据使用选项1,对经常更新的数据使用选项2?假设,我们正在存储文章,并且将诸如title,author和url之类的字段存储在具有通用密钥之类的JSON字符串中obj,并使用单独的key存储诸如view,投票和选民之类的字段?这样,通过单个READ查询,您可以获得整个对象,并且仍然可以快速更新对象的动态部分吗?可以通过在事务中读写整个对象来完成相对不频繁的JSON字符串字段更新。
2015年

2
据此:(instagram-engineering.tumblr.com/post/12202313862/…)根据内存消耗,建议将其存储在多个哈希中。因此,经过arun的优化,我们可以执行以下操作:1-进行多个散列存储json有效负载作为不经常更新的数据的字符串,以及2-进行多个散列存储json字段用于频繁更新的数据
Aboelnour

2
如果是option1,为什么还要将其添加到集合中?为什么我们不能简单地使用Get命令并检查return是否为nil。
实用语

8

给定答案的一些补充:

首先,如果您要高效地使用Redis哈希,则必须知道键计数的最大数量和值的最大大小-否则,如果它们破坏了hash-max-ziplist-value或hash-max-ziplist-entries,Redis会将其转换为实际值常用的键/值对。(请参见hash-max-ziplist-value,hash-max-ziplist-entries),从哈希选项中解脱确实是很糟糕的,因为Redis内部每个常用的键/值对每对都使用+90字节。

这意味着,如果您从选项二开始,而意外突破了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的答案,对选项一的评论具有误导性:

如果需要所有字段或多个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上了。

第二种选择很可能在第四个解决方案下获得解决,因为有疑问指出:

请记住,如果使用散列,则值长度是不可预测的。它们并不都是短的,例如上面的bio示例。

就像您已经说过的那样:第四个解决方案是每个属性最昂贵的+70字节。

我的建议是如何优化此类数据集:

您有两种选择:

  1. 如果您不能保证某些用户属性的最大大小,则可以使用第一个解决方案;如果内存问题至关重要,则可以在存储到Redis中之前压缩用户json。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.