【问题标题】:Redis strings vs Redis hashes to represent JSON: efficiency?Redis 字符串与 Redis 哈希来表示 JSON:效率?
【发布时间】:2013-04-28 19:37:47
【问题描述】:

我想将 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"

请记住,如果我使用哈希,则值长度是不可预测的。它们并不像上面的 bio 示例一样短。

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

【问题讨论】:

标签: json redis


【解决方案1】:

这篇文章可以在这里提供很多见解: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 解析速度很快,尤其是当您需要一次访问此对象的多个字段时。 缺点:只需要访问单个字段时速度较慢。

  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}
    

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

    优点:对象属性是成熟的 Redis 键,这对您的应用来说可能并不过分。 缺点:速度慢,占用更多内存,不被视为“最佳做法”。大量污染主键命名空间。

总体总结

选项 4 通常不是首选。选项 1 和 2 非常相似,而且都很常见。我更喜欢选项 1(一般来说),因为它允许您存储更复杂的对象(具有多层嵌套等)。选项 3 用于真正关心不污染主键命名空间(即,您不希望数据库中有很多键,并且您不关心诸如 TTL、键分片之类的东西。

如果我在这里有问题,请考虑发表评论并允许我在否决之前修改答案。谢谢! :)

【讨论】:

  • 对于选项#2,您说“当您需要访问对象中的所有/大部分字段时可能会更慢”。是否已经过测试?
  • hmget 是 O(n) 对于 n 字段 get 与选项 1 仍然是 O(1)。从理论上讲,是的,它更快。
  • 如何将选项 1 和 2 与哈希结合起来?对不经常更新的数据使用选项 1,对经常更新的数据使用选项 2?比如说,我们正在存储文章,我们将标题、作者和 url 等字段存储在 JSON 字符串中,并使用通用键(如obj)并使用单独的键存储视图、投票和选民等字段?这样,通过单个 READ 查询,您可以获得整个对象,并且仍然可以快速更新对象的动态部分?可以通过在事务中读取和写入整个对象来完成对 JSON 字符串中字段相对不频繁的更新。
  • 据此:(instagram-engineering.tumblr.com/post/12202313862/…) 就内存消耗而言,建议存储在多个哈希中。所以经过arun的优化,我们可以做到:1-为不经常更新的数据做多个hash存储jsonpayload为字符串,2-为经常更新的数据做多个hash存储json字段
  • 如果是option1,我们为什么要将它添加到集合中?为什么我们不能简单地使用 Get 命令并检查 return 是否不是 nil。
【解决方案2】:

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

选择选项 1:

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

选择选项 2:

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

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

【讨论】:

  • 选项 1 如果 并发修改 JSON 有效载荷预计不是一个好主意(非原子 @987654322 的经典问题@)。
  • 在 Redis 中将 json blob 存储为 json 字符串或字节数组的可用选项中哪个更有效?
【解决方案3】:

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

首先,如果你要有效地使用 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,您将在用户模型中的每个属性中获得 +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 的回答,选项一的 cmets 具有误导性:

如果您需要所有字段或多个 get/set 操作,hgetall/hmset/hmget 可以帮助您。

对于 BMiner 的回答。

第三个选项实际上非常有趣,对于 max(id)

但很多时候哈希只包含几个字段。当哈希值很小时,我们可以将它们编码为 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 字符串。无论哪种方式,您都可以压缩长用户属性。

【讨论】:

    【解决方案4】:

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

    我有一个专门用于此 Redis 客户端库 here 的 repo

    基本思想是在大小大于某个阈值时检测有效负载,然后将其 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
    
    
    
    Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
    WithCompressionBenchmark 668.2 ms 13.34 ms 27.24 ms - - - 4.88 MB
    WithoutCompressionBenchmark 1,387.1 ms 26.92 ms 37.74 ms - - - 2.39 MB

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-11-13
      • 1970-01-01
      • 1970-01-01
      • 2012-07-02
      • 1970-01-01
      • 1970-01-01
      • 2021-09-28
      • 1970-01-01
      相关资源
      最近更新 更多