Redis内存管理
Redis所有的数据都是在内存中的,本文介绍:
- 内存消耗分析
- 管理内存的原理分析
- 内存优化技巧
1.内存消耗
1.1内存使用统计
Redis自身使用内存的统计数据,可通过执行info memory命令获取内存相关指标
info_memory详细解释
属性名 | 属性说明 |
---|---|
user_memory | Redis分配的内存总量,也就是内存存储的所有数据内存占用量 |
userd_memory_human | 以可读的格式返回user_memory |
user_memory_rss | 以操作系统的角度显示Redis进程占用的物理内存空间总量 |
user_memory_peak | 内存使用的最大值,表示user_memory的峰值 |
user_memory_peak_human | 以可读的格式返回user_memory_peak |
user_memory_lua | lua引擎所消耗的内存大小 |
mem_fragmentaton_ratio | user_memory_rss/user_memory比值,表示内存的碎片率 |
mem_allocator | /Redis使用的内存分配器 |
需要重点关注的指标是:user_memory_rss,user_memory,mem_fragmentaton_ratio
当mem_fragmentaton_ratio>1时,说明user_memory_rss-user_memory多出的部分内存并没有用于数据存储,而是被内存碎片所消耗,如果比值比较大,说明碎片化严重
当mem_fragmentaton_ratio<1,说明操作系统把Redis内存交换到硬盘导致,出现这种情况要重点注意,硬盘处理速度远远慢于内存,Redis性能会急剧变差,甚至僵死。
1.2.Redis进程内内存消耗
Redis进程内存消耗主要包括
- 自身内存
- 对象内存
- 缓冲内存
- 内存碎片
自身内存
Redis空进程自身消耗内存很少,通常user_memory_rss在3M左右,user_memory在800k左右,一个空的redis进程消耗的内存可以忽略不计
对象内存
对象内存是Redis中占用内存最大的一块,存储着用户所有的数据。Redis所有的数据都是采用key-value数据类型,一个键包含两个对象:key对象和value对象。
对象的内存可以简单理解为:sizeof(keys)+sizeof(value)
缓存内存
缓存内存主要包括:客户端缓冲,复制积压缓存区和AOF缓存区
客户端缓冲是指所有接入到Redis服务器TCP连接的输入输出缓冲。输入缓冲无法控制,输出缓冲通过参数client-output-buffer-limit控制。客户端缓冲类型有如下三种:
- 普通客户端:除了复制和订阅的客户端以外的所有连接。Redis默认的配置是:client-output-buffer-limit normal 0 0 0,Redis默认对普通客户端没有进行输出缓冲区控制。但是要设置maxclients限制,特别是大量数据输出命令且数据无法推送到客户端时,容易造成Redis服务器内存突然飙升。
2)从客户端:主节点会为每个从节点单独创建一条连接用于命令复制,当主从节点之间延迟较高或者主节点挂载大量从节点时,这部分内存消耗将占用很大一部分。
3)订阅客户端:当使用发布订阅功能时,连接的客户端使用单独的输出缓冲区,当订阅服务的消息生产快于消费速度时,输出缓冲区会产生积压造成缓冲区空间溢出。
复制解压缓冲区:复制解压缓冲区整个主节点只有一个,所有从节点共享此缓冲区,因此可以设置较大的缓冲区空间,有效避免全量复制。
AOF缓冲区:Redis重写期间保存最近的写入命令,消耗内存取决于AOF重写的时间和写入命令量,一般消耗较少。
内存碎片
Redis默认采用的内存分配方式是jemalloc,可选的分配器还有glibc,tcmalloc。例如我们可能保存5KB对象时,采用8KB的块存储,而剩下的3KB内存变成了内存碎片不能再分配给其他对象使用。
容易产生内存碎片场景
- 频繁做更新操作
- 大量过期键删除
内存碎片常见解决方式 - 数据对齐:在条件允许的情况下尽量做数据对齐
- 安全重启:重启节点可以做到内存碎片重新整理
1.3子进程内存消耗
子进程内存消耗主要指执行AOF/RDB重写时,Redis创建的子进程内存消耗。Redis执行fork操作产生的子进程内存占用量对外表现为与父进程相同。Linux具有写时复制技术,父子进程共享相同的物理内存页,当父进程处理写请求时会对需要修改的页复制出一份副本完成写操作,而子进程依然读取fork时整个父进程的内存快照
2.内存管理
Redis主要通过控制内存上限和回收策略来实现内存管理
2.1设置内存上限
Redis使用maxmemory参数限制最大可用内存,限制内存两个目的:
- 用于缓存场景,当超过maxmemory时使用LRU等删除策略释放空间
- 防止所有内存超过服务器物理内存
2.2 动态调整内存上限
Redis的内存上限可以通过config set maxmemory进行动态设置
动态修改maxmemory,可以实现在当前服务器下动态伸缩Redis内存的目的
2.3内存回收策略
Redis内存回收机制主要体现在两个方面
- 删除到达过期时间的键对象
- 内存使用到达maxmemory上限时触发内存溢出控制策略
2.3.1删除过期键对象
Redis所有的键都可以设置过期属性,内存保存在过期字典中。
Redis采用惰性删除和定时任务删除机制来实现过期键的内存回收
- 惰性删除:惰性删除用于当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,会执行删除操作并返回空,这种方式存在内存泄漏的问题,如果键一直没有被访问将一直无法删除。
- 定时任务删除:Redis内存维护一个定时任务,默认每秒运行10次,定时任务中删除过期键采用了自适应算法,根据键的过期比例,使用快慢两种速率模式进行回收键
流程说明:
1)定时任务在每个数据库空间随机检查20个键,当发现过期时删除对应的键
2)如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或运行超时为止,慢模式下超时时间为25毫秒
3)如果之前回收键逻辑超时,则Redis触发内部事件之前再次以快模式运行回收过期键任务,快模式下超时时间为1毫秒且2秒内只能运行1次
4)快慢两种模式内部删除逻辑相同,只是执行超时时间不同
2.3.2 内存溢出控制策略
当Redis所使用内存达到maxmemeory上限时会触发相应的溢出内存策略。
Redis支持6种策略
- noeviction 默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息
- volatile-lru 根据LRU算法删除设置了超时属性的键,直到腾出足够空间为止
- allkeys-lru 根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止
- allkeys-random 随机删除所有的键
- volatile-random 随机删除过期键
- volatile-ttl 根据键值对象的ttl属性,删除最近将过期数据
3 内存优化
3.1 redisObject对象
Redis存储的所有值对象在内存中定义为redisObject结构体
type字段:表示当前对象使用的数据类型
encoding字段:表示Redis内部编码类型
lru字段:记录对象最后一次被访问的时间
refcount字段:记录当前对象,yu’o’g’n被引用的次数,用于通过引用次数回收内存,当refcount=0可以安全回收当前对象空间
*prf字段:与对象的数据内容有关,如果是整数,直接存储数据;否则表示指向数据的指针
3.2 缩减键值对象
降低Redis内存使用最直接的方式就是缩减键和值的长度
key长度:键值越短越好,可以采用单词缩写
value长度:精简业务对象,去除不必要字段,可以采用压缩算法压缩空间
3.3 共享对象池
共享对象池是指Redis内存维护的整数对象池,用于节约内存。因此在开发满座条件下尽量使用整数对象以节约内存
但是当设置maxmemory并开启LRU相关淘汰策略时,Redis禁止使用共享内存对象池
为什么开启maxmemeory和LRU淘汰策略后对象池失效?
- LRU算法需要获取对象最后被访问时间,以便淘汰最长未访问数据,每个对象最后访问时间存储在redisObject对象的lru字段。对象共享意味着多个引用共享同一个redisObject,这时lru字段也会被共享,导致无法获取每个对象的最后访问时间。如果没有设置maxmomery,直到内存被用尽Redis也不会触发淘汰内存回收,所以共享对象池可以正常工作
为什么只有整数对象池?
- 整数对象池复用几率大
- 对象共享的一个关键操作就是判断相等性,Redis只有比较算法复杂度是O(1),只保留一万个整数是为了防止对象池浪费
3.4 字符串优化
字符串对象是Redis内部最常用的数据类型。所有的键都是字符串类型,之对象除了整数之外都是使用字符串存储。
3.4.1 字符串结构
Redis自身实现的字符串结构特点:
- O(1)时间复杂度获取:字符串长度,已知长度,未用长度
- 可用于保存字节数组,支持安全二进制数据存储
- 内部实现空间预分配机制,降低内存再分配次数
- 惰性删除机制,字符串缩减后的空间不释放,作为预分配空间保留
3.4.2 预分配机制
字符串之所以采用预分配方式是为了防止修改操作不断重分配内存和字节数据拷贝。
3.4.3 字符串重构
字符串重构,指不一定把每份数据作为字符串整体存储,像json这样的数据可以使用hash结构,使用二级结构存储也能帮助我们节省内存。
3.5 编码优化
Redis针对每种数据结构可以采用至少两种编码方式来实现
Redis为什么对一种数据类型实现多种编码方式?
- 主要原因是Redis作者想通过不同编码实现效率和空间平衡。
3.6 控制键数量
存储相同的数据内容利用Redis的数据结构降低外层键的数量,也可以节省内存。