文章目录
一、内存消耗
1、内存使用统计:info memory
重点关注:used_memory_rss、used_memory、mem_fragmentation_ratio(比值)
当mem_fragmentation_ratio>1时,说明碎片率严重。
当mem_fragmentation_ratio<1时,说明Redis内存交换(swap)到硬盘导致,Redis性能会很差,甚至僵死。
2、内存消耗划分
Redis进程内消耗主要包括:自身内存(消耗非常少)+对象内存+缓冲内存+内存碎片
- 对象内存
对象内存是Redis内存占用最大的一块,存储着用户所有的数据。对象内存消耗可以简单理解为sizeOf(keys) + sizeof(values) - 缓冲内存:客户端缓冲、复制积压缓冲区、AOF缓冲区
客户端缓冲:指的是所有接入到Redis服务器TCP连接的输入输出缓冲。输入缓冲无法控制,最大空间为1G,如果超过将断开连接。输出缓冲通过参数client-output-buffer-limit控制。
复制积压缓冲区:主节点的写命令会缓冲到复制积压缓冲区,从节点从中读取,根据repl-backlog-size参数控制,默认1MB。
AOF:用于在Redis重写期间保存最近的写入命令。 - 内存碎片
Redis默认的内存分配器采用jemalloc,可选的分配器还有:glibc / tcmalloc。
出现高内存碎片问题时的解决办法
:1、数据对齐;2、安全重启:将碎片率过高的主节点转换为从节点,进行安全重启。
3、子进程内存消耗
子进程内存消耗:主要是指执行AOF、RDB重写时Redis创建的子进程内存消耗。Redis执行fork操作产生的子进程内存占用量对外表现为与父进程相同,理论上需要一倍的物理内存来完成重写操作。
子进程内存消耗总结如下:
- Redis产生的子进程并不需要消耗1倍的父进程内存,实际消耗根据期间写入命令量决定,但是依然要预留出一些内存防止溢出。
- 需要设置sysctl vm.overcommit_memory=1 允许内核可以分配所有的物理内存,防止Redis进程 执行fork时候因系统剩余内存不足而失败。
- 排查当前系统是否支持并开启THP机制(Transparent Huge Pages),如果开启建议关闭,防止copy-on-write期间内存过度消耗。如果在高并发的写场景下开启THP,子进程内存消耗可能是父进程的数倍,极易造成极其物理内存溢出,从而触发swap或者OOM killer
二、内存管理
Redis主要通过控制内存上限和回收策略实现内存管理。
1、设置内存上限:maxmemory
Redis使用maxmemory参数限制最大可用内存。Redis的内存上限也可以通过config set maxmemory
进程动态修改。
限制内存目的:
- 用于缓存场景,当超出内存上限maxmemory时使用LRU等删除策略释放空间;
- 防止所用内存超过服务器物理内存。
注意:maxmemory限制的是Redis实际使用的内存量,也就是used_memory 统计项对应内存。由于内存碎片率的存在,实际消耗的内存可能会比maxmemory设置的更大,实际使用时要小心这部分内存溢出。
2、内存回收策略
Redis内存回收机制主要体现在两个方面:
- 删除过期的键:
(1)惰性删除:客户端读取对象时发现过期了才删除。
(2)定时删除任务 - 内存使用达到maxmemory 上限时,处理内存溢出控制策略:
当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxMemory-policy
参数控制,内存溢出控制策略也可以采用config set maxmemory-policy {policy}
动态配置。
Redis支持六种内存溢出控制策略:
策略 | 说明 |
---|---|
noeviction | 默认策略,不会删除任何数据,拒绝所有写入操作返回错误信息 OOM,只响应读操作 |
volatile-LRU | 根据LRU 算法删除设置了超时属性的键,直到腾出足够空间为止 |
allkeys-LRU | 根据LRU 废删除键,不管数据有没有设置超时属性,直到腾出足够空间为止 |
allkeys-random | 随机删除所有键,直到腾出足够空间为止 |
volatile-random | 随机删除过期键,直到腾出足够空间为止 |
volatile-ttl | 根据键值对象的ttl 属性,删除最近将要过期数据 |
Redis4之后支持的内存策略:
策略 | 说明 |
---|---|
volatile-LFU | 根据LFU 算法来淘汰过期键。 |
allkeys-LFU | 根据LFU 算法删除键,不管是否超时,直到腾出足够空间 |
LRU 算法:
LRU是Least Recently Used
的缩写,即最近最少使用,常用于页面置换算法,是为虚拟页式存储管理服务的。
实现 LRU 算法除了需要 key/value 字典外,还需要附加一个链表,链表中的元素按照一定的顺序进行排列。当空间满的时候,会踢掉链表尾部的元素。当字典的某个元素被访问时,它在链表中的位置会被移动到表头。所以链表的元素排列顺序就是元素最近被访问的时间顺序。位于链表尾部的元素就是不被重用的元素,所以会被踢掉。位于表头的元素就是最近刚刚被人用过的元素,所以暂时不会被踢。
LFU 算法:
LFU 是Least Frequently Used
的缩写,表示按最近的访问频率进行淘汰,它比 LRU 更加精准地表示了一个key 被访问的热度
频繁
执行回收内存成本很高,建议线上Redis内存工作在maxmemory>used_memory状态下,避免频繁内存回收开销
。
三、内存优化
Redis存储的所有值对象在内部定义为redisObject结构体,结构如下:
*ptr字段:与对象的数据内容相关,如果是整数,直接存储数据;否则表示指向数据的指针。Redis在3。0之后对值对象是字符串且长度<=39字段的数据,内部编码为embstr类型,字符串sds和redisObject一起分配,从而只要一次内存操作即可。
高并发写入场景中,在条件允许情况下,建议字符串长度控制在39字节以内,减少创建redisObject内存分配次数,从而提高性能。
1、缩减键值对象
缩减key和value长度。在内存紧张的情况下,可以使用通用压缩算法压缩json、xml后在存入redis,从而降低内存占用,比如GZIP压缩、google的Snappy压缩工具(性能更好)。
2、共享对象池
共享对象池是指在redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占用16个字节,甚至超过了整数自身空间消耗。所以redis内存维护一个[0-9999]整数对象池,用于节约内存。除了整数值对象,其他类型如list、hash、set、zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽可能使用整数对象以节省内存。
需要注意的是对象池并不是只要存储[0-9999]的整数就可以工作。当设置maxmemory并启用LRU相关淘汰策略:volatile-lru,allkeys-lru时,Redis禁止使用共享对象池。
3、字符串优化
Redis没有使用C语言的字符串类型而是自己实现了字符串结构,内部简单动态字符串(simple dynamic string,SDS)。结构如图:
-
Redis字符串结构特点:
(1)O(1)时间复杂度获取:字符串长度、已用长度、未用长度;
(2)可用于保存字节数组、支持安全二进制数据存储;
(3)内部实现空间预分配机制,降低内存再分配次数;
(4)惰性删除机制,字符串减后的空间不释放,作为预分配空间保留; -
预分配机制:字符串之所以采用预分配的方式是防止修改操作需要不断重分配内存和字节数据拷贝。但同样会造成内存浪费。尽量减少字符串频繁修改操作如append、setrange,改为直接使用set修改字符串,降低预分配带来的内存浪费各内存碎片化。
-
字符串重构:指不一定把每份数据作为 字符串整体存储,像json这样的数据可以使用hash结构,使用二级结构存储也能帮我们节省内存。
4、编码优化
使用object encoding {key}
命令获取编码类型。如下表:
- 控制编码类型:编码类型转换在Redis写入数据时自动完成,这个转换过程是不可逆的。转换规则只能从小内存编码向大内存编码转换。
- ziplist 编码
主要是为了节约内存,因此所有数据都是采用线性连续的内存结构。ziplist 编码是应用范围最广的一种,可以分别作为hash/ list / zset /类型的底层数据结构实现。首先从ziplist 编码结构开始分析,它的内部结构类似这样:
ziplist结构字段含义:一个ziplist可以包含多个entry(元素),每个entry保存具体的数据。
(1)zlbytes: 记录整个压缩列表所占字节长度,方便重新调整ziplist空间。
(2) zltail:记录距离尾节点的偏移量,方便尾节点弹出操作。
(3)zllen: 记录压缩链表节点数量
(4)entry: 记录具体的节点,长度根据实际存储的数据而定。
(5) zlend: 记录列表结尾,占用一个字节。
ziplist结构特点如下:
(1)内部表现为数据紧凑排列的一块连续内存数组
(2)可以模拟双向链表结构,以O(1)时间复杂度入队和出队
(3)新增删除操作涉及内存重新分配或释放,加大了操作的复杂性
(4)读写操作涉及复杂的指针移动,最坏时间复杂度是O(n2)
(5)适合存储小对象和长度有限的数据
- intset编码
intset编码是集合(set)的类型编码的一种,内部表现为存储有序、不重复的整数集。
intset数据结构在集合长度可控的基础上,性能很好,因此当使用整数集合时尽量使用intset编码。
5、控制键数量
过多的键同样会消耗大量内存,通过在客户端预估键规模,把大量键分组映射到多个hash结构中减低键的数量。
hash结构降低键数量分析:
- 根据键规模在客户端通过分组映射到一组hash对象中,如存在100万个键,可以映射到1000个hash中,每个hash保存1000个元素。
- hash的field可用于记录原始key字符串,方便哈希查找。
- hash的value保存原始值对象,确保不要超过hash-max-ziplist-value限制。
通过上面的测试数据,可以说明:
- 同样的数据使用ziplist编码的hash类型存储比string类型节约内存。
- 节省内存量随着value空间的减少越来越明显。
- hash-ziplist类型比string类型写入耗时,但随着value空间的减少,耗时逐渐降低。
注意:
1.hash类型节省内存的原理是使用ziplist编码,如果使用hashtable编码方式反而会增加内存消耗。
2. hash重构后所有的键无法再使用超时(expire)和LRU淘汰机制自动删除,需要手动维护删除
3. 对于大对象,如1KB以上的对象,使用hash-ziplist结构控制键数量反而得不偿失
4. 对于大量小对象的存储场景,非常适合使用ziplist编码的hash类型控制键的规模来降低内存