Redis【有与无】【UR3】小聚合数据类型的特殊编码

本文基于Redis 6.0.9版本,前提至少 Redis 3.0或更高版本。

此页面正在进行中。 当前,这只是您应该检查内存问题的清单。

目录

1.小聚合数据类型的特殊编码

1.1.使用32bit实例

1.2.Bit和byte级操作

1.3.尽可能使用hashes

1.4.使用hashes在Redis上抽象出内存效率很高的纯key-value存储

1.5.内存分配


1.小聚合数据类型的特殊编码

从Redis 2.2开始,对许多数据类型进行了优化,以使用较少的空间来达到特定大小。 当Hashes,Lists,仅由整数组成的Sets和Sorted Sets,当小于元素的给定数量且最大元素大小时,它们以非常节省内存的方式进行编码,使用的内存减少多达10倍(其中平均节省5倍省的时间)。

从用户和API的角度来看,这是完全透明的。 由于这是CPU /内存的折衷方案,因此可以使用以下redis.conf指令来调整特殊编码类型的最大元素数和最大元素大小。

hash-max-ziplist-entries 512
hash-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512

如果特殊编码的值超出了配置的最大大小,Redis将自动将其转换为普通编码。 对于较小的值,此操作非常快,但是如果更改设置以便对大得多的聚合类型使用经过特殊编码的值,建议您运行一些基准测试以检查转换时间。

1.1.使用32bit实例

由于指针很小,使用32位目标编译的Redis每个键使用的内存要少得多,但是这样的实例将被限制为最大内存使用量为4 GB。 要将Redis编译为32位二进制文件,请使用32bit。 RDB和AOF文件在32位和64位实例之间兼容(当然,在小端字节序和大端字节序之间也是兼容的),因此您可以从32位切换到64位,反之亦然。

1.2.Bit和byte级操作

Redis 2.2引入了新的bit 和byte 级别的操作:GETRANGESETRANGEGETBITSETBIT。 使用这些命令,您可以将Redis字符串类型视为随机访问数组。

例如,如果您有一个用唯一的渐进整数标识用户的应用程序,则可以使用位图将有关用户订阅的信息保存在邮件列表中,设置要订阅的位并清除不订阅的位,或者 另一种方式。 在Redis实例中,拥有1亿用户的数据将仅占用12 MB的RAM。 您可以使用GETRANGE 和 SETRANGE进行相同操作,以便为每个用户存储一个字节的信息。 这只是一个例子,但是实际上可以使用这些新原语在很小的空间中为许多问题建模。

1.3.尽可能使用hashes

小hashes被编码在很小的空间中,因此您应尽可能使用hashes来表示数据。 例如,如果您在Web应用程序中有代表用户的对象,则不要在名称,姓氏,电子邮件,密码中使用不同的键,而应在所有必填字段中使用单个hashes。

1.4.使用hashes在Redis上抽象出内存效率很高的纯key-value存储

基本上,可以使用Redis对纯key-value存储进行建模,其中值仅可以是字符串,这不仅比Redis普通键具有更高的内存效率,而且比内存缓存高得多。

让我们从一个事实开始:几个键比包含一个带有几个字段的hashes的单个键使用更多的内存。 这怎么可能? 我们使用技巧。 从理论上讲,为了保证我们在恒定时间内执行查询(在大O表示法中也称为O(1)),有必要在平均情况下使用具有恒定时间复杂度的数据结构,例如hash table。

但是很多时候,hashes仅包含几个字段。 当哈希较小时,我们可以将其编码为O(N)数据结构,例如带有长度前缀键值对的线性数组。 由于我们仅在N较小时执行此操作,因此HGET和HSET命令的摊销时间仍为O(1):一旦包含的元素数量太大,hashes将转换为真实的hash table(您可以 在redis.conf中配置限制)。

这不仅从时间复杂度的角度来看效果很好,而且从固定时间的角度来看效果也不错,因为key-value对的线性数组恰好可以很好地与CPU缓存配合使用(它具有更好的缓存位置 比hash table)。

但是,由于hashes字段和值不会(始终)表示为功能齐全的Redis对象,因此哈希字段不能像真实键一样具有关联的生存时间(到期),并且只能包含字符串。 但是我们对此表示同意,无论如何,这是设计哈希数据类型API时的意图(我们比功能更信任简单性,因此不允许嵌套数据结构,因为不允许单个字段过期)。

因此,hashes可以提高内存效率。 当存在一组相关字段时,使用散列表示对象或为其他问题建模时,这非常有用。 但是,如果我们有一个简单的键值业务呢?

想象一下,我们想将Redis用作许多小对象的缓存,这些对象可以是JSON编码的对象,小的HTML片段,简单的key -> boolean值等等。 基本上任何东西都是带有小键和值的 string -> string映射。

现在,假设要缓存的对象已编号,例如:

  • object:102393
  • object:1234
  • object:5

这就是我们所能做的。 每次执行SET操作以设置新值时,我们实际上将密钥分为两部分,一部分用作密钥,另一部分用作哈希的字段名称。 例如,名为"object:1234"的对象实际上分为:    

  • 一个名为object的Key:12
  • 一个名为34的字段

因此,我们使用除最后两个字符以外的所有字符作为键,并使用最后两个字符作为哈希字段名称。 要设置密钥,我们使用以下命令:

HSET object:12 34 somevalue

如你所见,每个哈希将以100个字段结尾,这是CPU和内存节省之间的最佳折衷方案。

需要注意的另一件事是非常重要的,在这种模式下,无论我们缓存的对象数量如何,每个哈希都将具有或多或少的100个字段。 这是因为我们的对象将始终以数字结尾,而不是随机字符串。 在某种程度上,最终数量可以视为隐式预分片的一种形式。

小数字呢? 像对象:2? 我们仅使用“object:”作为键名,并使用整数作为哈希字段名来处理这种情况。 因此,object:2和object:10都将在键“object:”内结束,但是其中一个作为字段名称“2”,另一个作为“10”。

这样可以节省多少内存?

我使用以下Ruby程序来测试其工作原理:

require 'rubygems'
require 'redis'

UseOptimization = true

def hash_get_key_field(key)
    s = key.split(":")
    if s[1].length > 2
        {:key => s[0]+":"+s[1][0..-3], :field => s[1][-2..-1]}
    else
        {:key => s[0]+":", :field => s[1]}
    end
end

def hash_set(r,key,value)
    kf = hash_get_key_field(key)
    r.hset(kf[:key],kf[:field],value)
end

def hash_get(r,key,value)
    kf = hash_get_key_field(key)
    r.hget(kf[:key],kf[:field],value)
end

r = Redis.new
(0..100000).each{|id|
    key = "object:#{id}"
    if UseOptimization
        hash_set(r,key,"val")
    else
        r.set(key,"val")
    end
}

64位实例的结果:

  • UseOptimization设置为true:1.7 MB已用内存
  • UseOptimization设置为false; 11 MB的已用内存

这是一个数量级,我认为这使Redis或多或少地是那里存储效率最高的普通键值存储。

警告:要使此方法起作用,请确保在您的redis.conf中具有以下内容

hash-max-zipmap-entries 256

还要记住,请根据您的键和值的最大大小设置以下字段:

hash-max-zipmap-value 1024

每当哈希超过指定的元素数量或元素大小时,它将被转换为真实的哈希表,并且内存节省将丢失。

您可能会问,为什么不在常规密钥空间中隐式地执行此操作,这样我就不必在乎了? 原因有两个:一是我们倾向于明确权衡取舍,而这是许多因素之间的明显取舍:CPU,内存,最大元素大小。 第二个问题是顶级密钥空间必须支持很多有趣的事情,例如过期,LRU数据等等,因此以一般方式执行此操作不切实际。

但是 Redis 的方法是,用户必须理解事物是如何工作的,以便他能够选择最好的折衷方案,并准确地理解系统将如何运行。

1.5.内存分配

为了存储用户密钥,Redis最多分配与maxmemory设置启用的内存一样多的内存(但是可能会有少量额外的分配)。

确切的值可以在配置文件中设置,也可以稍后通过 CONFIG SET设置(有关更多信息,请参阅使用内存作为LRU缓存)。 关于Redis如何管理内存,应注意以下几点:

  • Redis的不会内存总是释放(返回)到OS密钥将被删除时。 关于Redis,这不是什么特别的事情,而是大多数malloc()实现的工作方式。 例如,如果您用5GB大小的数据填充实例,然后删除相当于2GB大小的数据,则“驻留集大小(Resident Set Size)”(也称为RSS,即进程消耗的内存页数)可能仍然是大约5GB,即使Redis声称用户内存约为3GB。 发生这种情况是因为基础分配器无法轻松释放内存。 例如,通常大多数已删除的密钥与仍然存在的其他密钥分配在相同的页面中。
  • 上一点意味着您需要根据峰值内存使用量来配置内存。 如果你的工作量有时需要10GB,即使大多数情况下5GB可以做到,你也需要准备10GB。
  • 但是分配器很聪明,能够重用空闲的内存块,因此,释放5GB数据集中的2GB后,再次开始添加更多密钥时,您会看到RSS(常驻集大小)保持稳定并且不会增长更多 ,因为您最多添加2GB的其他密钥。 分配器基本上是在尝试重用先前(逻辑上)释放的2GB内存。
  • 由于所有这些原因,当你的内存使用量峰值比当前使用的内存大得多时,碎片率是不可靠的。 碎片计算为实际使用的物理内存(RSS值)除以当前使用的内存量(作为Redis执行的所有分配的总和)。 因为RSS反映了峰值内存,所以当(虚拟)已用内存较低时(由于释放了许多 keys / values),但是RSS较高时,RSS / mem_used的比率将非常高。

如果未设置maxmemory,则Redis将在其认为合适的情况下继续分配内存,因此它可以(逐渐)耗尽所有可用内存。 因此,通常建议配置一些限制。 您可能还需要将maxmemory-policy设置为noeviction(这在某些旧版本的Redis中不是默认值)。

当达到极限时,它会使Redis返回写命令的内存不足错误很可能导致应用程序错误,但不会由于内存不足而使整个计算机死机。

待续...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

琴 韵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值