我想将JSON有效负载存储到redis中。我有两种方法可以做到这一点:
使用简单的字符串键和值 key:user,value:payload(整个JSON blob,可以是100-200 KB)
SET user:1 payload
使用哈希
HSET user:1 username "someone"
HSET user:1 location "NY"
HSET user:1 bio "STRING WITH OVER 100 lines"
请记住,如果我使用哈希值,则值的长度是不可预测的。它们并非都是短的,例如上面的生物例子。
哪个内存更有效?使用字符串键和值,还是使用哈希?
答案 0 :(得分:367)
这篇文章可以提供很多见解:http://redis.io/topics/memory-optimization
有许多方法可以在Redis中存储对象数组(剧透:我喜欢大多数用例的选项1):
将整个对象作为JSON编码的字符串存储在单个键中,并使用集合(或列表,如果更合适)跟踪所有对象。例如:
INCR id:users
SET user:{id} '{"name":"Fred","age":25}'
SADD users {id}
一般来说,这可能是大多数情况下最好的方法。如果对象中有很多字段,则对象不会与其他对象嵌套,并且您一次只能访问一小部分字段,最好选择选项2.
优势:被视为“良好做法”。每个对象都是一个完整的Redis密钥。 JSON解析很快,特别是当您需要同时访问此Object的许多字段时。 缺点:当您只需访问一个字段时速度较慢。
将每个Object的属性存储在Redis哈希中。
INCR id:users
HMSET user:{id} name "Fred" age 25
SADD users {id}
优势:被视为“良好做法”。每个对象都是一个完整的Redis密钥。无需解析JSON字符串。 缺点:当您需要访问Object中的所有/大多数字段时,可能会更慢。此外,无法轻松存储嵌套对象(对象内的对象)。
将每个Object作为JSON字符串存储在Redis哈希中。
INCR id:users
HMSET users {id} '{"name":"Fred","age":25}'
这允许你巩固一点,只使用两个键而不是许多键。明显的缺点是你不能在每个用户对象上设置TTL(以及其他东西),因为它只是Redis哈希中的一个字段,而不是一个完整的Redis密钥。
优点:JSON解析速度很快,尤其是当您需要同时访问此Object的许多字段时。减少主要名称空间的“污染”。 缺点:当您拥有大量对象时,与#1相同的内存使用量。当您只需要访问单个字段时,比#2慢。可能不被视为“良好做法”。
将每个Object的每个属性存储在专用密钥中。
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,键分片等等。
如果我在这里遇到问题,请考虑发表评论并允许我在弃权前修改答案。谢谢! :)
答案 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字节。
我建议如何优化此类数据集:
你有两个选择:
如果您无法保证某些用户属性的最大大小,而不是第一个解决方案,并且内存问题至关重要 在redis中存储之前压缩用户json。
如果您可以强制所有属性的最大大小。 您可以设置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 |