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

时间:2013-05-04 14:08:19

标签: json redis

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

  1. 使用简单的字符串键和值 key:user,value:payload(整个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"

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

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

4 个答案:

答案 0 :(得分:367)

这篇文章可以提供很多见解: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. 将每个Object的属性存储在Redis哈希中。

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

    优势:被视为“良好做法”。每个对象都是一个完整的Redis密钥。无需解析JSON字符串。 缺点:当您需要访问Object中的所有/大多数字段时,可能会更慢。此外,无法轻松存储嵌套对象(对象内的对象)。

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

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

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

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

  4. 将每个Object的每个属性存储在专用密钥中。

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

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

    优势:对象属性是完整的Redis密钥,对您的应用来说可能不会有点过分。 缺点:缓慢,使用更多内存,而不是“最佳实践”。很多污染主键名称空间。

  5. 总体摘要

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

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

答案 1 :(得分:139)

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

选择选项1:

  • 如果您在大多数访问中使用大部分字段。
  • 如果可能的密钥存在差异

选择选项2:

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

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

答案 2 :(得分:5)

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

首先,如果您要有效地使用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)&lt;的数据集,第三个选项实际上非常有趣。 has-max-ziplist-value这个解决方案具有O(N)复杂度,因为,令人惊讶的是,Reddis将小哈希存储为长度/键/值对象的数组式容器!

  

但很多次哈希只包含几个字段。当散列较小时,我们可以只将它们编码为O(N)数据结构,就像具有长度前缀键值对的线性数组。由于我们只在N很小时才这样做,因此HGET和HSET命令的分摊时间仍为O(1):一旦包含的元素数量增加太多,哈希将被转换为真实的哈希表

但是你不应该担心,你会非常快地打破hash-max-ziplist-entries,而你现在实际上是在解决方案1号。

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

  

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

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

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

你有两个选择:

  1. 如果您无法保证某些用户属性的最大大小,而不是第一个解决方案,并且内存问题至关重要 在redis中存储之前压缩用户json。

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

答案 3 :(得分:0)

我们在生产环境中遇到了类似的问题,我们想出了一个想法,如果负载超过某个阈值 KB,则对其进行 gzip 压缩。

我有一个专门用于这个 Redis 客户端库 here

基本思想是检测有效载荷,如果大小大于某个阈值,然后对其进行 gzip 和 base-64,然后将压缩的字符串作为普通字符串保存在 redis 中。检索时检测字符串是否为有效的 base-64 字符串,如果是,则对其进行解压缩。

整个压缩和解压过程都是透明的,而且你会获得接近 50% 的网络流量

压缩基准测试结果


BenchmarkDotNet=v0.12.1, OS=macOS 11.3 (20E232) [Darwin 20.4.0]
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.201
  [Host] : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT DEBUG


<头>
方法 平均 错误 StdDev Gen 0 Gen 1 第 2 代 已分配
使用压缩基准 668.2 毫秒 13.34 毫秒 27.24 毫秒 - - - 4.88 MB
没有压缩基准 1,387.1 毫秒 26.92 毫秒 37.74 毫秒 - - - 2.39 MB