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

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

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

    . key:user, value:payload(整个JSON blob,可以是100- 200kb

    SET user:1 payload < / p >

  2. < p >使用散列

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

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

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

173941 次浏览

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

选择选项1:

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

选择选项2:

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

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

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

在Redis中有很多方法来存储对象数组(扰流板:我喜欢选项1对于大多数用例):

  1. 将整个对象作为json编码的字符串存储在一个键中,并使用set(或list,如果更合适)跟踪所有对象。例如:

    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. 将每个对象存储为Redis散列中的JSON字符串。

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

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

    优势: JSON解析非常快,特别是当你需要一次访问这个对象的多个字段时。减少主键名称空间的“污染”。缺点:当你有很多对象时,内存使用量与#1相同。当您只需要访问一个字段时,比#2要慢。可能不被认为是一个“好的实践”。< / p >

  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,键分片,或任何东西)。

如果我在这里写错了什么,请考虑留下评论,并允许我在投票之前修改答案。谢谢!:)

对给定答案的补充:

首先,如果你想有效地使用Redis哈希,你必须知道 一个键计数的最大数量和值的最大大小-否则,如果他们打破哈希-max-ziplist-value或哈希-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的答案,选项一的评论具有误导性:

Hgetall /hmset/hmget可以在需要所有字段或多个get/set操作时救场。

对于BMiner的答案。

第三个选项其实很有趣,对于max(id) <这个解决方案有O(N)的复杂性,因为,令人惊讶的是,Reddis将小哈希存储为长度/键/值对象的类似数组的容器!

但是很多时候哈希只包含几个字段。当哈希值很小时,我们可以将它们编码为O(N)数据结构,就像带有长度前缀键值对的线性数组一样。因为我们只在N很小的时候才这样做,HGET和HSET命令的平摊时间仍然是O(1):一旦哈希表中包含的元素数量增长太多,哈希表就会被转换成真正的哈希表

但是你不用担心,你很快就会打破hash-max-ziplist-entries你现在实际上得到了第一个解。

第二个选择很可能是第四个解决方案,因为问题指出:

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

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

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

你有两个选择:

    如果你不能保证某些用户属性的最大大小,那么你就去第一个解决方案,如果内存问题是至关重要的 在存储到redis之前,压缩用户json
  1. 如果你可以强制所有属性的最大大小。 然后你可以设置哈希-max-ziplist-entries/value,并使用哈希作为每个用户表示的一个哈希,或者从Redis指南的这个主题:https://redis.io/topics/memory-optimization中作为哈希内存优化,并将用户存储为json字符串。无论哪种方式,你都可以压缩长用户属性

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

我有一个回购只专用于这个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




< span style=" font - family:宋体;">是< / th > < span style=" font - family:宋体;"> < / th >错误 < span style=" font - family:宋体;"> StdDev < / th > < span style=" font - family:宋体;"> Gen 0 < / th > < span style=" font - family:宋体;">创1 < / th > < span style=" font - family:宋体;"> < / th >第2代 < span style=" font - family:宋体;"> < / th >分配 . .
方法
WithCompressionBenchmark 13.34 ms - < / td > < span style=" font - family:宋体;"> - < / td > < span style=" font - family:宋体;"> - < / td > 4.88 MB
WithoutCompressionBenchmark 1387.1 ms - < / td > < span style=" font - family:宋体;"> - < / td > < span style=" font - family:宋体;"> - < / td >

要在Redis中存储JSON,您可以使用Redis JSON模块。

这会给你:

  • 完全支持JSON标准
  • 用于在文档中选择/更新元素的JSONPath语法
  • 文档存储为树状结构中的二进制数据,允许快速访问子元素
  • 所有JSON值类型的类型化原子操作

< a href = " https://redis。Io /docs/stack/json/" rel="nofollow noreferrer">https://redis.io/docs/stack/json/ .

https://developer.redis.com/howtos/redisjson/getting-started/

https://redis.com/blog/redisjson-public-preview-performance-benchmarking/