Redis内存分析

在了解 Redis 的 5 种对象类型的用法和特点的基础上,进一步了解 Redis 的内存模型,对 Redis 的使用有很大帮助。比如估算Redis内存使用量,内存优化占用,阻塞问题处理。

一、Redis内存统计

Redis提供内存统计命令,在客户端通过 redis-cli 连接服务器后,通过 info 命令可以查看内存使用情况:info memory。
这里写图片描述

其中info命令可以显示很多Redis服务器信息,包括服务器基本信息、CPU、内存、持久化、客户端连接信息等等;memory 是参数,表示只显示内存相关的信息。
其中几个重要信息:

  • used_memory:表示Redis分配器分配的内存总量,单位是字节,包括使用的虚拟内存(swap)
  • used_memory_rss:表示Redis进程占用系统的内存,单位是字节。除了分配器分配的内存之外,used_memory_rss 还包括进程运行本身需要的内存、内存碎片等,但是不包括虚拟内存
  • mem_fragmentation_ratio:表示内存碎片比率,该值是 used_memory_rss / used_memory 的比值。mem_fragmentation_ratio 一般大于 1,且该值越大,内存碎片比例越大;mem_fragmentation_ratio<1,说明 Redis 使用了虚拟内存,由于虚拟内存的媒介是磁盘,比内存速度要慢很多。一般来说,mem_fragmentation_ratio 在 1.03 左右是比较健康的状态(对于 jemalloc 来说)。
  • mem_allocator:Redis 使用的内存分配器,在编译时指定;可以是 libc 、jemalloc 或者 tcmalloc,默认是 jemalloc。

二、Redis内存划分

除了数据之外,Redis本身也会占用内存,Redis 的内存占用主要可以划分为以下几个部分:
1.数据
作为内存数据库,数据是最主要的部分;这部分占用的内存会统计在 used_memory 中。
Redis 使用键值对存储数据,其中的值(对象)包括 5 种类型,即字符串、哈希、列表、集合、有序集合。
这 5 种类型是 Redis 对外提供的,实际上,在 Redis 内部,每种类型可能有 2 种或更多的内部编码实现。
Redis 在存储对象时,并不是直接将数据扔进内存,而是会对对象进行各种包装:如 RedisObject、SDS 等。
2.Redis进程运行占用的内存
Redis 主进程本身运行肯定需要占用内存,如代码、常量池等等;这部分内存大约几兆,在大多数生产环境中与 Redis 数据占用的内存相比可以忽略。
除了主进程外,Redis 创建的子进程运行也会占用内存,如 Redis 执行 AOF、RDB 重写时创建的子进程。这部分内存不属于 Redis 进程,也不会统计在 used_memory 和 used_memory_rss 中。
3.缓冲内存
缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF 缓冲区等;其中,客户端缓冲区存储客户端连接的输入输出缓冲;复制积压缓冲区用于部分复制功能;AOF 缓冲区用于在进行 AOF 重写时,保存最近的写入命令。
这部分内存由jemalloc分配,因此会统计在 used_memory 中。
4.内存碎片
内存碎片是 Redis 在分配、回收物理内存过程中产生的。内存碎片不会统计在 used_memory 中。
如果 Redis 服务器中的内存碎片已经很大,可以通过安全重启的方式减小内存碎片:因为重启之后,Redis 重新从备份文件中读取数据,在内存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片。

Redis数据存储的细节

关于 Redis 数据存储的细节,涉及到内存分配器(如 jemalloc)、简单动态字符串(SDS)、RedisObject、5 种对象类型及内部编码。
下图是执行命令 set hello world 时,所涉及到的数据模型:
这里写图片描述
dictEntry:Redis 是 Key-Value 数据库,因此对每个键值对都会有一个 dictEntry,里面存储了指向 Key 和 Value 的指针;next 指向下一个 dictEntry,与本 Key-Value 无关。
Key:Key(”hello”)并不是直接以字符串存储,而是存储在 SDS 结构中。
RedisObject:Value(“world”)既不是直接以字符串存储,也不是像 Key 一样直接存储在 SDS 中,而是存储在 RedisObject 中。实际上,不论 Value 是 5 种类型的哪一种,都是通过 RedisObject 来存储的;而 RedisObject 中的 type 字段指明了 Value 对象的类型,ptr 字段则指向对象所在的地址。
jemalloc:默认的内存分配器。无论是 DictEntry 对象,还是 RedisObject、SDS 对象,都需要内存分配器(如 jemalloc)分配内存进行存储。

下面来分别介绍 jemalloc、RedisObject、SDS、对象类型及内部编码:
1.jemalloc
Redis 在编译时便会指定内存分配器;内存分配器可以是 libc 、jemalloc 或者 tcmalloc,默认是 jemalloc。
jemalloc 的优势体现在减小内存碎片方面。jemalloc 在 64 位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当 Redis 存储数据时,会选择大小最合适的内存块进行存储。
jemalloc 划分的内存单元如下图所示:
这里写图片描述
2.RedisObject
RedisObject 对象非常重要,Redis 对象的类型、内部编码、内存回收、共享对象等功能,都需要 RedisObject 支持,由上一节知,redisObject包含5个字段:
这里写图片描述

  • type
    type 字段表示对象的类型,占 4 个比特;目前包括 REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
    当我们执行 type 命令时,便是通过读取 RedisObject 的 type 字段获得对象的类型;如下图所示:
    这里写图片描述

  • encoding
    encoding 表示对象的内部编码,占 4 个比特。对于 Redis 支持的每种类型,都有至少两种内部编码,例如对于字符串,有 int、embstr、raw 三种编码。
    通过 encoding 属性,Redis 可以根据不同的使用场景来为对象设置不同的编码,大大提高了 Redis 的灵活性和效率。
    通过 object encoding 命令,可以查看对象采用的编码方式,如下图所示:
    这里写图片描述

  • lru
    lru 记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同(如 4.0 版本占 24 比特,2.6 版本占 22 比特)。
    通过对比 lru 时间与当前时间,可以计算某个对象的空转时间;object idletime 命令可以显示该空转时间(单位是秒)。object idletime 命令的一个特殊之处在于它不改变对象的 lru 值。
    这里写图片描述
    lru 值还与 Redis 的内存回收有关系。如果 Redis 打开了 maxmemory 选项,且内存回收算法选择的是 volatile-lru 或 allkeys—lru,那么当 Redis 内存占用超过 maxmemory 指定的值时,Redis 会优先选择空转时间最长的对象进行释放。
  • refcount
    refcount 记录的是该对象被引用的次数,类型为整型。refcount 的作用,主要在于对象的引用计数和内存回收。
    当创建新对象时,refcount 初始化为 1;当有新程序使用该对象时,refcount 加 1;当对象不再被一个新程序使用时,refcount 减 1;当 refcount 变为 0 时,对象占用的内存会被释放。
    Redis 中被多次使用的对象(refcount>1),称为共享对象。Redis 为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。
    这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。
    共享对象的具体实现:Redis 的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和 CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。
    对于整数值,判断操作复杂度为 O(1);对于普通字符串,判断复杂度为 O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为 O(n^2)。
    虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。
    就目前的实现来说,Redis 服务器在初始化时,会创建 10000 个字符串对象,值分别是 0~9999 的整数值;当 Redis 需要使用值为 0~9999 的字符串对象时,可以直接使用这些共享对象。10000 这个数字可以通过调整参数 REDIS_SHARED_INTEGERS(4.0 中是 OBJ_SHARED_INTEGERS)的值进行改变。
    共享对象的引用次数可以通过 object refcount 命令查看,如下图所示。命令执行的结果页佐证了只有 0~9999 之间的整数会作为共享对象。
    这里写图片描述
  • ptr
    ptr 指针指向具体的数据,如前面的例子中,set hello world,ptr 指向包含字符串 world 的 SDS。

    综上所述,RedisObject 的结构与对象类型、编码、内存回收、共享对象都有关系。
    一个 RedisObject 对象的大小为 16 字节:4bit+4bit+24bit+4Byte+8Byte=16Byte。

3.SDS
Redis 没有直接使用 C 字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,而是使用了 SDS。SDS 是简单动态字符串(Simple Dynamic String)的缩写。
SDS 的结构如下:
这里写图片描述
其中,buf 表示字节数组,用来存储字符串;len 表示 buf 已使用的长度;free 表示 buf 未使用的长度。
这里写图片描述
通过 SDS 的结构可以看出,buf 数组的长度=free+len+1(其中 1 表示字符串结尾的空字符)。
SDS 在 C 字符串的基础上加入了 free 和 len 字段,带来了很多好处:

  • 获取字符串长度:SDS 是 O(1),C 字符串是 O(n)。
  • 缓冲区溢出:SDS 由于记录了长度,相应的 API 在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。
  • 修改字符串时内存的重分配:对于 C 字符串,如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。
    而对于 SDS,由于可以记录 len 和 free,因此解除了字符串长度和空间数组长度之间的关联,可以在此基础上进行优化。
    空间预分配策略(即分配内存时比实际需要的多)使得字符串长度增大时重新分配内存的概率大大减小;惰性空间释放策略使得字符串长度减小时重新分配内存的概率大大减小。
  • 存取二进制数据:SDS 可以,C 字符串不可以。因为 C 字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等)。
    内容可能包括空字符串,因此 C 字符串无法正确存取;而 SDS 以字符串长度 len 来作为字符串结束标识,因此没有这个问题。

在Redis对象存储中,一律使用SDS代替c字符串。除了存储对象,SDS 还用于存储各种缓冲区。只有在字符串不会改变的情况下,如打印日志时,才会使用 C 字符串。

猜你喜欢

转载自blog.csdn.net/weixin_41172473/article/details/82259889
今日推荐