Redis内存模型

Redis是目前最火的数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站高并发不可或缺的一部分。

我们在使用Redis时,会接触到Redis的5种对象类型(字符串、哈希、列表、集合、有序集合)。在了解了Redis的5种对象类型的用法和特点的基础上,进一步了解Redis的内存模型,对Redis的使用会有很大的帮助,例如:

1、估计Redis的内存使用量。目前为止,内存的使用成本任然相对较高,使用内存不能无所顾忌,根据需求合理的评估Redis的内存使用量,选择合适的机器配置,可以在满足需求的情况下节约成本。

2、优化内存占用。了解Redis内存模型可以选择合适的的数据模型和编码,更好的利用Redis的内存。

3、分析解决问题。当Redis出现阻塞、内存占用等问题时,尽快发现导致问题的原因,便于分析解决问题。

一、Redis内存统计

在客户端通过redis-cli连接服务器后,通过info命令可以查看内存使用情况:

info memory

其中,info命令可以显示redis服务器的许多信息,包括服务器的基本信息、CPU、内存、持久化和客户端连接信息等等,memory是参数,表示内存相关信息。

返回的信息中比较重要的信息说明:

(1)used_memory:Redis分配器分配的内存总量(单位是字节),包括使用的虚拟内存(swap)。used_memory_human只是显示的更友好。

(2)used_memory_rss:Redis进程占据操作系统的内存(单位字节),与top和ps命令看到的值是一样的;除了分配器分配的内存外,used_memory_rss还包括进程运行本身需要的内存、内存碎片,但是不包括虚拟内存。

因此,used_memory和used_memory_rss,前者是从Redis的角度得到的量,后者是从操作系统的角度得到的量。二者之所以有所不同,一方面是因为内存碎片和redis进程运行需要占用内存,使得前者可能比后者小,另一方面虚拟内存的存在,使得前者可能比后者大。

由于在实际的应用中,Redis的数据量会比较大,此时进程运行占用的内存与Redis数据量和内存碎片相比都会小的多。因此,used_memory和used_memory_rss的比例,成了衡量Redis内存碎片率的参数,设个参数就是mem_fragmentation_ratio。

(3)mem_fragmentation_ratio:内存碎片比例,该值是used_memory_rss/used_memory的比值。

mem_fragmentation_ratio一般大于1,该值越大,内存碎片比例越大。mem_fragmentation_ratio<1,说明redis使用了虚拟内存,由于虚拟内存的介质是磁盘,比内存的速度慢很多,当这种情况出现时,应该及时排查,如果内存不足时应该及时处理,入增加redis节点、增加Redis服务器的内存、优化应用等。

一般来说,mem_fragmentation_ratio在1.03左右是比较健康的状态。

(4)mem_allocator:Redis使用内存分配器,在编译时指定。可以是libc、jemalloc或者tcmalloc,默认是jemalloc。

二、Reids内存划分

Redis作为内存数据库,在内存中存储的主要是数据(键值对),处了数据以外,Redis其他部分也会占用内存。

Reids的内存占用主要划分为以下几个部分:

1、数据

作为数据库,数据是主要部分,该部分占用的内存会统计在used_memory中。Redis使用键值对存储数据,其中的值包括5种类型,即字符串、哈希、列表、集合、有序集合。这5中类型是Redis对外提供的,实际,在redis内部,每种类型可能会有两种或更多的内部编码实现。此外,Redis在存储对象进行各种包装:如redisObject、SDS等。

2、进程本身需要内存

Redis主进程本身运行肯定需要占用内存,如代码、常量池等,在大多数的生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配,因此不会统计在used_memory中。

处了主进程外,Redis创建的子进程运行也会占用内存,如Redis执行AOF、ROB重写时创建的子进程。当然这部分内存不属于Redis进程,也不会统计在used_memory和used_memory_rss中。

3、缓冲内存

缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等,其中,客户端缓冲存储客户端连接的输入输出缓冲;复制积压缓冲用于部分复制功能;AOF缓存区用于AOF重写时,保存最近的写入命令。在了解相应的功能之前,不需要知道这些缓存的细节。这部分内存由jemalloc分配,会统计在used_memory中。

4、内存碎片

内存碎片是Redis在分配、回收物理内存过程中产生的。例如,如果读数据的更改频繁,而且数据之间的大小相差很大,可能导致Redis释放的空间在物理内存中没有释放,但Redis又无法有效利用,这就形成了内存碎片,内存碎片不会统计在used_memory中。

内存碎片的产生于对数据进行的操作、数据的特点等都有关系。此外,与使用的内存分配器也有关系。如果内存分配器设计合理,可以尽量减少内存碎片的产生。

如果Redis服务器中的内存碎片已经很大,可以通过安全重启的方式减少内存碎片,因为重启之后,Redis重新从备份文件读取数据,在内存中重排,为每个数据重新选择合适的内存单元,减小内存碎片。

三、Redis数据存储的细节

1、概述

关于redis数据存储的席间,涉及到内存分配器(如jemalloc)、简单动态字符串(SDS)、5中对象类型及内部编码、redisObject。我们先看下这几个概念之间的关系。

下面是执行set hello world时,所涉及到的数据模型。

(1)dictEntry:Redis是Key-Vaue数据库,因此对每个键值对都会有一个dictEntry,里面存储了指向Key和Value的指针;next指向下一个dictEetry,与本Key-Value无关。

(2)Key:Key(“hello”)并不是直接以字符串存储,二是存储在SDS结构中。

(3)redisObject:Value(“world”)既不是直接以字符串存储,也不是想Key那样直接存储在SDS中,而是存储在redisObject中。实际上,不论Value是5种类型中的哪一种,都是通过redisObject来存储的,而redisObject中的type字段指明了Value对象类型,ptr字段则指向对象所在的地址。不过可以看出,字符串对象虽然经过了redisObject的包装,但任然需要通过SDS存储。

实际上,redisObject除了type和ptr字段以外,还有其他的字段,如用于指定对象内部编码的字段等。

(4)jemalloc:无论是DictEntry对象还是redisObject、SDS对象,都需要内存分配器(如jemalloc)分配内存进行存储。以dictEntry对象为例,有3个指针组成,在64位机器下占24个字节,jemalloc会为他分配32字节大小的内存单元。

下面来分辨介绍jemalloc、redisObject、SDS、对象类型及内部编码。

2、jemalloc

redis在编译时便会指定尾部存储器,内存分配器可以是libc、jemalloc或者tcmalloc,默认是jemalloc。

jemollac作为redis的默认内存分配器,在减少内存碎片方面做的相对比较好。jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围,每个范围又划分了许多内存块单位,当redis存储数据时,会选择大小最合适的内存块进行存储。

jemollac划分的内存单元如下图所示:

例如,如果需要存储大小为130字节的对象,jemollac会将其放入160字节的内存单元中。

3、redisObject

前面说到,redis对象有5种类型,无论是那种类型,redis都不会直接存储,二是通过redisObject对象进行存储。

redisObject对象非常重要,redis对象的类型、内部编码、内存回收、共享对象等功能,都需要redisObject支持。

redisObject的定义如下:

typedef struct redisObject {

  unsigned type:4;

  unsigned encoding:4;

  unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

  int refcount;

  void *ptr;

} robj;

redisObject的每个字段的含义和作用如下:

(1)type

type字段表示对象的类型,占4个比特,目前包括REDIS_STRING(字符串)、REDIS_LIst(列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。

当我们执行type命令时,便是通过读取redisObject的type字段获得对象的类型,如图:

(2)encoding

encoding表示对象的内部编码,占4个比特。

对于Redis支持的每种类型,都至少有两种内部编码,例如对于字符串,有int、embstr、raw三种编码。通过encoding属性。redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了redis的灵活性和效率。以列表对象为例,有压缩列表和双端列表两种编码方式,如果列表中的元素较少,Redis倾向于使用压缩列表进行存储,因为压缩列表占用的内存更少,而且比双端列表可以更快的载入。当列表的元素较多时,压缩列表就会转换为更适合存储大量元素的双端列表。

通过object encoding命名,可以查看对象的编码方式,如下:

(3)lru

lru记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同(4.0版本24比特,2.6版本22比特)。

通过对比lru时间与当前时间,可以计算某个对象的空转时间,object idletime命令可以显示该空转时间(秒)。object idletime命令的一个特殊之处在于他不改变对象lru的值。

lru值除了通过object idletime命令打出以外,还与redis内存回收有关系:如果redis打开了maxmemory选项,且内存回收算法选择的是volatile-lru或allkeys-lru,name当redis内存占用草果maxmemory指定的值时,redis会优先选择空转时间最长的对象进行释放。

Redis中多次使用的对象,称为共享对象。Redis为了节省内存,当一些对象重复出现时,新的程序不会创建新的对象,而是任然使用原来的对象。这个被重复利用的对象,就是共享对象。目前共享对象仅支持整数型的字符串对象。

--共享对象的具体实现

Redis的共享对象目前只支持整数值的字符串对象,子所以如此,实际上是对内存和CPU的平衡,共享对象虽然降低内存消耗,但是判断两个对象是否相等,确需要消耗额外的时间,对于整数值,判断操作复杂度为O(1);对于普通字符串,判断复杂度为O(n);对于哈希、列表、集合和有序集合,判断的复杂度为O(n2)。

就目前的实现来说,redis的服务器在初始化时,会创建10000个字符串对象,值分别是0-9999的整数值。当redis需要使用值为0-9999的字符串对象时,可以直接使用共享对象。10000个数字可以通过调整参数REDIS_SHARED_INTEGERS的值进行改变。

共享对象的引用次数可以通过object refcount命令查看,如图:

(5)ptr

ptr指针指向具体数据,如前面的例子,set hello world,ptr指向包含字符串world的SDS。

(6)总结

redisObject的结构与对象类型、编码、内存回收、共享对象都偶有关系。一个redisObject对象的大小为16字节。

4、SDS

redis没有直接使用c字符串(用空字符‘\0’结尾的字符数组)作为默认的字符串表示,而是采用了SDS。SDS是简单动态字符串的缩写。

(1)SDS结构

SDS结构如下:

struct sdshdr {

    int len;

    int free;

    char buf[];

};

其中buf表示字节数组,来存储字符串;len表示buf已使用的长度,free表示buf为使用的长度。

通过SDS的结构可以看出,buf数组的长度=free+len+1(1表示字符串结尾的空字符)。所以一个SDS结构占据的空间为:free所占长度+len所占长度+buf数组长度=4+4+free+len+9.

(2)SDS与C串的比较

SDS在C字符串的基础上加入了free和len字段,带来了很多好处:

a、获取字符串长度:SDS是O(1),C字符串是O(n)

b、缓存区溢出:使用C字符串的API时,如果字符串长度增加而忘记了重新分配内存,很容易造成缓存区的溢出;而SDS由于记录了长度,相应的API在可能造成缓存区溢出时会自动重新分配内存,杜绝了缓存区溢出。

c、修改字符串时内存的重分配:对于C字符串,如果要修改字符串,必须要重新分配内存(先释放在申请),因为如果没有重新分配,字符串的长度增大胡造成内存缓存区溢出,字符串长度减小时会造成内存泄露。而对于SDS,由于可以记录len和free,因此减除了字符串和空间数组长度之间的关联,可以在此基础上进行优化。空间预分配策略(分配的内存比实际需要的多)使得字符串长度增加是重新分配内存的概率大大减小。惰性空间释放策略使得字符串长度减小时重新分配的概率大大减小。

d、存取二进制数据:SDS可以,C字符串不可以。因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片),内容可能包括空字符串,因此C字符串无法正确存储。而SDS以字符串长度len来作为字符串结束的标识,因此没有这个问题。

此外,由于SDS中的bug仍然使用了C字符串(即以‘\0’结尾),因此SDS可以使用C字符串库中的部分函数。但是需要主要的是,只有SDS用来存储文本数据才可以这样使用,在存放二进制数据时则不行。

四、redis的对象类型与内部编码

前面已经说过,redis支持5中对象类型,而且每种结构都至少有两种编码。这样做的好处在于:一方面接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响,另一方面可以根据不同的应用场景切换内部编码,提高效率。

你可能感兴趣的:(Redis内存模型)