Redis是目前最火的数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站高并发不可或缺的一部分。
我们在使用Redis时,会接触到Redis的5种对象类型(字符串、哈希、列表、集合、有序集合)。在了解了Redis的5种对象类型的用法和特点的基础上,进一步了解Redis的内存模型,对Redis的使用会有很大的帮助,例如:
1、估计Redis的内存使用量。目前为止,内存的使用成本任然相对较高,使用内存不能无所顾忌,根据需求合理的评估Redis的内存使用量,选择合适的机器配置,可以在满足需求的情况下节约成本。
2、优化内存占用。了解Redis内存模型可以选择合适的的数据模型和编码,更好的利用Redis的内存。
3、分析解决问题。当Redis出现阻塞、内存占用等问题时,尽快发现导致问题的原因,便于分析解决问题。
一、Redis内存统计
在客户端通过redis-cli连接服务器后,通过info命令可以查看内存使用情况:
info memory
![](https://img.haomeiwen.com/i9199159/816865cfd1479a19.png)
其中,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时,所涉及到的数据模型。
![](https://img.haomeiwen.com/i9199159/c359c5ba781bdecc.png)
(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划分的内存单元如下图所示:
![](https://img.haomeiwen.com/i9199159/65f2a3c4fb7deac8.png)
例如,如果需要存储大小为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字段获得对象的类型,如图:
![](https://img.haomeiwen.com/i9199159/4878108feacbde36.png)
(2)encoding
encoding表示对象的内部编码,占4个比特。
对于Redis支持的每种类型,都至少有两种内部编码,例如对于字符串,有int、embstr、raw三种编码。通过encoding属性。redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了redis的灵活性和效率。以列表对象为例,有压缩列表和双端列表两种编码方式,如果列表中的元素较少,Redis倾向于使用压缩列表进行存储,因为压缩列表占用的内存更少,而且比双端列表可以更快的载入。当列表的元素较多时,压缩列表就会转换为更适合存储大量元素的双端列表。
通过object encoding命名,可以查看对象的编码方式,如下:
![](https://img.haomeiwen.com/i9199159/ec1b8ff1f913178d.png)
(3)lru
lru记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同(4.0版本24比特,2.6版本22比特)。
通过对比lru时间与当前时间,可以计算某个对象的空转时间,object idletime命令可以显示该空转时间(秒)。object idletime命令的一个特殊之处在于他不改变对象lru的值。
![](https://img.haomeiwen.com/i9199159/d5ad8035cd0d979f.png)
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命令查看,如图:
![](https://img.haomeiwen.com/i9199159/e6c0d9a0ab712489.png)
(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中对象类型,而且每种结构都至少有两种编码。这样做的好处在于:一方面接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响,另一方面可以根据不同的应用场景切换内部编码,提高效率。
网友评论