326

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

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

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

4

4 回答 4

465

这篇文章可以在这里提供很多见解: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}
    

    根据上面的文章,这个选项几乎从来都不是首选(除非 Object 的属性需要有特定的TTL什么的)。

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

总体总结

选项 4 通常不是首选。选项 1 和 2 非常相似,而且都很常见。我更喜欢选项 1(一般来说),因为它允许您存储更复杂的对象(具有多层嵌套等)。当您真正关心不污染主键名称空间时使用选项 3(即您不想要那里)成为数据库中的很多键,而您不关心诸如 TTL、键分片或其他什么的东西)。

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

于 2013-09-24T20:15:22.613 回答
182

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

选择选项 1:

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

选择选项 2:

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

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

于 2013-05-04T14:23:08.297 回答
10

给定答案集的一些补充:

首先,如果您要有效地使用 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 个解决方案。

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

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

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

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

你有两个选择:

  1. 如果你不能保证某些用户属性的最大大小,那么你会选择第一个解决方案,如果内存很重要,那么在存储到 redis 之前压缩用户 json。

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

于 2018-05-28T13:21:35.663 回答
1

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

我在这里有一个专门用于这个 Redis 客户端库的仓库

基本思想是如果大小大于某个阈值,则检测有效负载,然后将其 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


方法 意思是 错误 标准差 0代 第一代 第 2 代 已分配
WithCompressionBenchmark 668.2 毫秒 13.34 毫秒 27.24 毫秒 - - - 4.88 MB
无压缩基准 1,387.1 毫秒 26.92 毫秒 37.74 毫秒 - - - 2.39 MB
于 2021-05-11T08:20:56.940 回答