Redis-内存优化

Redis 内存优化

小的聚合类型数据的特殊编码处理

Redis2.2版本及以后,存储集合数据的时候会采用内存压缩技术,以使用更少的内存存储更多的数据。如Hashes,Lists,Sets和Sorted Sets,当这些集合中的所有数都小于一个给定的元素,并且集合中元素数量小于某个值时,存储的数据会被以一种非常节省内存的方式进行编码,使用这种编码理论上至少会节省10倍以上内存(平均节省5倍以上内存)。并且这种编码技术对用户和redis 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

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

使用32位的redis(压缩指针)

使用32位的redis,对于每一个key,将使用更少的内存,因为32位程序,指针占用的字节数更少。

注意以下几点

  • 32位的redis整个实例使用的内存将被限制在4G以下。
  • 使用make 32bit命令编译生成32位的redis。
  • RDB和AOF文件是不区分32位和64位的(包括字节顺序),所以你可以使用64位的reidis恢复32位的RDB备份文件,相反亦然。

位级别和字节级别的操作

Redis 2.2引入了新的位级别和字节级别的操作: GETRANGE, SETRANGE, GETBIT 和 SETBIT。
使用这些命令,你可以把redis的字符串当做一个随机访问的(字节)数组。例如你有一个应用,用户ID是连续的自增整数,你可以使用一个位图(bitmap)标记用户的性别,使用1表示男性,0表示女性,或者其他的方式。这样的话,1亿用户将仅消耗12 M的内存。你可以使用同样的方法,使用 GETRANGE 和 SETRANGE 命令为每个用户存储一个字节的信息。这只是一个例子,实际上你可以使用这些原始数据类型解决更多问题。

尽可能使用散列表(hashes)

小散列是在非常小的空间中编码的,所以每次可能的时候都应该尝试使用哈希来表示数据。例如,如果您在web应用程序中有一个表示用户的对象,不要为name、name、email、password设置不同的key,而应该使用一个包含所有必需字段的散列表来存储。

使用散列结构高效存储抽象的键值对

一些key存储了一个对象的多个字段要比一个散列表存储对象的多个字段占用更多的内存。这怎么可能?
因为一个线性数组通常会被CPU的缓存更好的命中(线性数组有更好的局部性),从而提升了访问的速度。
现在我们假设要缓存的对象使用数字后缀进行编码,如:

  • object:102393
  • object:1234
  • object:5
    我们可以这样做。每次SET的时候,把key分为两部分,第一部分当做一个key,第二部当做散列表字段。比如“object:1234”,分成两部分:
  • a Key named object:12
  • a Field named 34
    我们使用除最后2个数字的部分作为key,最后2个数字做为散列表的字段。使用命令:
    HSET object:12 34 somevalue
    如你所见,每个散列表将(理论上)包含100个字段,这是CPU资源和内存资源之间的一个折中。
    另一个需要你关注的是在这种模式下,无论缓存多少对象,每个散列表都会分配100个字段。因为我们的对象总是以数字结尾,而不是一个随机的字符串。从某些方面来说,这是一种隐性的预分片。
    对于小数字怎么处理?比如object:2,我们采用object:作为Key,所有剩下的数字作为一个Field。所以object:2object:10都会被存储到key为object:的散列表中,但是一个使用2作为Field,一个使用10作为Field。
    这种方式将节省多少内存?
    我使用了下面的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
}

在redis2.2的64位版本上测试结果:

  • 当开启优化时使用内存1.7M
  • 当未开启优化时使用内存11M
    从结果看出,这是一个数量级的优化,我认为这种优化使redis成为最出色的键值缓存。

特别提示:
要使上面的程序较好的工作,别忘记设置你的redis:
hash-max-zipmap-entries 256
相应的最大键值长度设置:
hash-max-zipmap-value 1024

每次散列表的元素数量或者值超过了阈值,散列将被扩展为一张真正的散列表进行存储,此时节约存储的优势就没有了。

或许你想问,你为什么不自动将这些key进行转化以提高内存利用率?
有两个原因:

  • 第一是因为我们更倾向于让这些权衡明确,而且必须在很多事情之间权衡:CPU,内存,最大元素大小限制。
  • 第二是顶级的键空间支持很多有趣的特性,比如过期,LRU算法,所以这种做法并不是一种通用的方法.

Redis的一贯风格是用户必须理解它是如何运作的,必须能够做出最好的选择和权衡,并且清楚它精确的运行方式。

内存分配

为了存储用户数据,当设置了maxmemory后Redis会分配几乎和maxmemory一样大的内存(可能还会有其他方面的一些内存分配)。
精确的值可以在配置文件中设置,或者在启动后通过 CONFIG SET 命令设置。

Redis内存管理方面,需要注意以下几点:

  • 当某些缓存被删除后Redis并不是总是立即将内存归还给操作系统。
    这并不是redis所特有的,而是C库函数malloc()动态内存分配的特性。例如你缓存了5G的数据,然后删除了2G数据,从操作系统看,redis可能仍然占用了5G的内存(这个内存叫RSS,后面会用到这个概念),即使redis已经明确声明只使用了3G的空间。这是因为redis使用的底层内存分配器不会这么简单的就把内存归还给操作系统,可能是因为已经删除的key和没有删除的key在同一个页面(page),这样就不能把完整的一页归还给操作系统。
  • 你应该基于你可能会用到的 最大内存 来指定redis的最大内存
    如果你的程序时不时的需要10G内存,即便在大多数情况是使用5G内存,你也需要指定最大内存为10G。
  • 内存分配器是智能的,可以复用用户已经释放的内存
    所以当使用的内存从5G降低到3G时,你可以重新添加更多的key,而不需要再向操作系统申请内存。分配器将复用之前已经释放的2G内存。
  • 当内存使用峰值远大于当前使用的内存时,碎片率是不可靠的(波动很大)
    碎片率 = 实际占用内存 / 当前使用内存
    因为实际占用内存(RSS)就是内存使用峰值(peak memory),所以当大部分key被释放的时候,(实际占用内存/当前使用内存)就会很高,即碎片率很大。
posted @ 2020-09-10 19:59  明子健  阅读(161)  评论(0)    收藏  举报