深入学习Redis(1):Redis内存模型
Redis是目前最火爆的内存数据库之一通过在内存中读写数据大大提高了读写速度可以说Redis是实现网站高并发不可或缺的一部分。我们使用Redis时会接触Redis的5种对象类型字符串、哈希、列表、集合、有序集合丰富的类型是Redis相对于Memcached等的一大优势。在了解Redis的5种对象类型的用法和特点的基础上进一步了解Redis的内存模型对Redis的使用有很大帮助例如1、估算Redis内存使用量。目前为止内存的使用成本仍然相对较高使用内存不能无所顾忌根据需求合理的评估Redis的内存使用量选择合适的机器配置可以在满足需求的情况下节约成本。2、优化内存占用。了解Redis内存模型可以选择更合适的数据类型和编码更好的利用Redis内存。3、分析解决问题。当Redis出现阻塞、内存占用等问题时尽快发现导致问题的原因便于分析解决问题。这篇文章主要介绍Redis的内存模型以3.0为例包括Redis占用内存的情况及如何查询、不同的对象类型在内存中的编码方式、内存分配器(jemalloc)、简单动态字符串(SDS)、RedisObject等然后在此基础上介绍几个Redis内存模型的应用。在后面的文章中会陆续介绍关于Redis高可用的内容包括主从复制、哨兵、集群等等欢迎关注。系列文章深入学习Redis1Redis内存模型深入学习Redis2持久化深入学习Redis3主从复制深入学习Redis4哨兵深入学习Redis5集群目录一、Redis内存统计二、Redis内存划分1、数据或者称为对象2、进程本身运行需要的内存3、缓冲内存4、内存碎片三、Redis数据存储的细节1、概述2、jemalloc3、redisObject4、SDS四、Redis的对象类型与内部编码1、字符串2、列表3、哈希4、集合5、有序集合五、应用举例1、估算Redis内存使用量2、优化内存占用3、关注内存碎片率六、参考文献一、Redis内存统计工欲善其事必先利其器在说明Redis内存之前首先说明如何统计Redis使用内存的情况。在客户端通过redis-cli连接服务器后后面如无特殊说明客户端一律使用redis-cli通过info命令可以查看内存使用情况1info memory其中info命令可以显示redis服务器的许多信息包括服务器基本信息、CPU、内存、持久化、客户端连接信息等等memory是参数表示只显示内存相关的信息。返回结果中比较重要的几个说明如下1used_memoryRedis分配器分配的内存总量单位是字节包括使用的虚拟内存即swapRedis分配器后面会介绍。used_memory_human只是显示更友好。2used_memory_rssRedis进程占据操作系统的内存单位是字节与top及ps命令看到的值是一致的除了分配器分配的内存之外used_memory_rss还包括进程运行本身需要的内存、内存碎片等但是不包括虚拟内存。因此used_memory和used_memory_rss前者是从Redis角度得到的量后者是从操作系统角度得到的量。二者之所以有所不同一方面是因为内存碎片和Redis进程运行需要占用内存使得前者可能比后者小另一方面虚拟内存的存在使得前者可能比后者大。由于在实际应用中Redis的数据量会比较大此时进程运行占用的内存与Redis数据量和内存碎片相比都会小得多因此used_memory_rss和used_memory的比例便成了衡量Redis内存碎片率的参数这个参数就是mem_fragmentation_ratio。3mem_fragmentation_ratio内存碎片比率该值是used_memory_rss / used_memory的比值。mem_fragmentation_ratio一般大于1且该值越大内存碎片比例越大。mem_fragmentation_ratio1说明Redis使用了虚拟内存由于虚拟内存的媒介是磁盘比内存速度要慢很多当这种情况出现时应该及时排查如果内存不足应该及时处理如增加Redis节点、增加Redis服务器的内存、优化应用等。一般来说mem_fragmentation_ratio在1.03左右是比较健康的状态对于jemalloc来说上面截图中的mem_fragmentation_ratio值很大是因为还没有向Redis中存入数据Redis进程本身运行的内存使得used_memory_rss 比used_memory大得多。4mem_allocatorRedis使用的内存分配器在编译时指定可以是 libc 、jemalloc或者tcmalloc默认是jemalloc截图中使用的便是默认的jemalloc。二、Redis内存划分Redis作为内存数据库在内存中存储的内容主要是数据键值对通过前面的叙述可以知道除了数据以外Redis的其他部分也会占用内存。Redis的内存占用主要可以划分为以下几个部分1、数据作为数据库数据是最主要的部分这部分占用的内存会统计在used_memory中。Redis使用键值对存储数据其中的值对象包括5种类型即字符串、哈希、列表、集合、有序集合。这5种类型是Redis对外提供的实际上在Redis内部每种类型可能有2种或更多的内部编码实现此外Redis在存储对象时并不是直接将数据扔进内存而是会对对象进行各种包装如redisObject、SDS等这篇文章后面将重点介绍Redis中数据存储的细节。2、进程本身运行需要的内存Redis主进程本身运行肯定需要占用内存如代码、常量池等等这部分内存大约几兆在大多数生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配因此不会统计在used_memory中。补充说明除了主进程外Redis创建的子进程运行也会占用内存如Redis执行AOF、RDB重写时创建的子进程。当然这部分内存不属于Redis进程也不会统计在used_memory和used_memory_rss中。3、缓冲内存缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等其中客户端缓冲存储客户端连接的输入输出缓冲复制积压缓冲用于部分复制功能AOF缓冲区用于在进行AOF重写时保存最近的写入命令。在了解相应功能之前不需要知道这些缓冲的细节这部分内存由jemalloc分配因此会统计在used_memory中。4、内存碎片内存碎片是Redis在分配、回收物理内存过程中产生的。例如如果对数据的更改频繁而且数据之间的大小相差很大可能导致redis释放的空间在物理内存中并没有释放但redis又无法有效利用这就形成了内存碎片。内存碎片不会统计在used_memory中。内存碎片的产生与对数据进行的操作、数据的特点等都有关此外与使用的内存分配器也有关系如果内存分配器设计合理可以尽可能的减少内存碎片的产生。后面将要说到的jemalloc便在控制内存碎片方面做的很好。如果Redis服务器中的内存碎片已经很大可以通过安全重启的方式减小内存碎片因为重启之后Redis重新从备份文件中读取数据在内存中进行重排为每个数据重新选择合适的内存单元减小内存碎片。三、Redis数据存储的细节1、概述关于Redis数据存储的细节涉及到内存分配器如jemalloc、简单动态字符串SDS、5种对象类型及内部编码、redisObject。在讲述具体内容之前先说明一下这几个概念之间的关系。下图是执行set hello world时所涉及到的数据模型。图片来源https://searchdatabase.techtarget.com.cn/7-20218/1dictEntryRedis是Key-Value数据库因此对每个键值对都会有一个dictEntry里面存储了指向Key和Value的指针next指向下一个dictEntry与本Key-Value无关。2Key图中右上角可见Key”hello”并不是直接以字符串存储而是存储在SDS结构中。3redisObjectValue(“world”)既不是直接以字符串存储也不是像Key一样直接存储在SDS中而是存储在redisObject中。实际上不论Value是5种类型的哪一种都是通过redisObject来存储的而redisObject中的type字段指明了Value对象的类型ptr字段则指向对象所在的地址。不过可以看出字符串对象虽然经过了redisObject的包装但仍然需要通过SDS存储。实际上redisObject除了type和ptr字段以外还有其他字段图中没有给出如用于指定对象内部编码的字段后面会详细介绍。4jemalloc无论是DictEntry对象还是redisObject、SDS对象都需要内存分配器如jemalloc分配内存进行存储。以DictEntry对象为例有3个指针组成在64位机器下占24个字节jemalloc会为它分配32字节大小的内存单元。下面来分别介绍jemalloc、redisObject、SDS、对象类型及内部编码。2、jemallocRedis在编译时便会指定内存分配器内存分配器可以是 libc 、jemalloc或者tcmalloc默认是jemalloc。jemalloc作为Redis的默认内存分配器在减小内存碎片方面做的相对比较好。jemalloc在64位系统中将内存空间划分为小、大、巨大三个范围每个范围内又划分了许多小的内存块单位当Redis存储数据时会选择大小最合适的内存块进行存储。jemalloc划分的内存单元如下图所示图片来源http://blog.csdn.net/zhengpeitao/article/details/76573053例如如果需要存储大小为130字节的对象jemalloc会将其放入160字节的内存单元中。3、redisObject前面说到Redis对象有5种类型无论是哪种类型Redis都不会直接存储而是通过redisObject对象进行存储。redisObject对象非常重要Redis对象的类型、内部编码、内存回收、共享对象等功能都需要redisObject支持下面将通过redisObject的结构来说明它是如何起作用的。redisObject的定义如下不同版本的Redis可能稍稍有所不同1234567typedefstructredisObject {unsigned type:4;unsigned encoding:4;unsigned lru:REDIS_LRU_BITS;/* lru time (relative to server.lruclock) */intrefcount;void*ptr;} robj;redisObject的每个字段的含义和作用如下1typetype字段表示对象的类型占4个比特目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。当我们执行type命令时便是通过读取RedisObject的type字段获得对象的类型如下图所示2encodingencoding表示对象的内部编码占4个比特。对于Redis支持的每种类型都有至少两种内部编码例如对于字符串有int、embstr、raw三种编码。通过encoding属性Redis可以根据不同的使用场景来为对象设置不同的编码大大提高了Redis的灵活性和效率。以列表对象为例有压缩列表和双端链表两种编码方式如果列表中的元素较少Redis倾向于使用压缩列表进行存储因为压缩列表占用内存更少而且比双端链表可以更快载入当列表对象元素较多时压缩列表就会转化为更适合存储大量元素的双端链表。通过object encoding命令可以查看对象采用的编码方式如下图所示5种对象类型对应的编码方式以及使用条件将在后面介绍。