前言

Redis的数据都存储在内存中,所以本篇文章将学习Redis的内存机制,以帮助定位Redis的内存相关问题。

正文

一. 查看Redis中的内存

Redis提供了info memory指令来查看Redis的内存情况,但是在查看Redis中的内存之前,先通过一段代码来造成RedisOOM,代码片段如下。

long count = 0L;
while (true) {
    byte[] bytes = new byte[1024];
    RBucket<byte[]> curBucket = redissonClient.getBucket(String.valueOf(count++));
    curBucket.set(bytes);
}

由于事先已经将Redis的最大内存设置为了10MB,所以上述代码片段运行一小会儿就会造成Redis产生OOMRedisson抛出的OOM异常如下。

此时使用info memory指令查看Redis内存情况,如下所示。

下表是对上图的字段的说明。

字段说明
used_memoryRedis使用的内存总量。包含对象内存缓冲内存虚拟内存自身内存,不包含内存碎片
used_memory_rssRedis进程占用的内存总量。包含对象内存缓冲内存自身内存内存碎片,不包含虚拟内存
used_memory_peakused_memory达到过的峰值。
used_memory_luaLua引擎占用的内存。
maxmemory用户配置的Redis的最大内存。
maxmemory_policyRedis达到最大内存时的淘汰策略。
mem_fragmentation_ratioused_memory_rss / used_memory。代表Redis内存的碎片化率,值越大,表示Redis的内存碎片越多。
mem_allocatorRedis使用的内存分配器。默认为jemalloc

注:以_human结尾的字段是添加了单位的可读方式显示。

二. used_memory详解

info memory指令的结果中的used_memory表示Redis实际使用的内存,通常由对象内存缓冲内存自身内存组成,可以由下图进行概括。

1. 对象内存

已知Redis中存储数据时使用的键和值均为对象,Redis中的对象共五种(详见Redis-对象类型),当存储数据时,会根据存储场景将数据存储为不同的对象,此时Redis的内存分配器会为这些对象分配内存空间。

2. 缓冲内存

Redis中的缓冲区主要有:客户端缓冲区AOF缓冲区复制积压缓冲区,这些缓冲区的内存消耗可以概括如下。

  • 客户端缓冲区:又分为客户端输入缓冲区客户端输出缓冲区。客户端输入缓冲区用于暂存客户端输入的指令,当一次性写入大量指令,或者服务端负载过高时,客户端输入缓冲区会持续增高,客户端输入缓冲区的最大容量为1GB。客户端输出缓冲区用于保存指令执行后的返回结果;
  • AOF缓冲区:Redis进行AOF持久化时会先将Redis指令写入缓冲区中,然后再根据AOF的写磁盘策略在合适的时间点将缓存内容刷到磁盘文件中;
  • 复制积压缓冲区:用于主从同步。关于主从同步,会在后续的文章中介绍。

3. 自身内存

主要指进行RDB或者AOF持久化时,Redis创建子进程的内存消耗。

三. mem_fragmentation_ratio详解

mem_fragmentation_ratio表示Redis内存的碎片化率,所谓内存碎片,就是Redis占用着但是又没有用于存储数据的内存。mem_fragmentation_ratio = used_memory_rss / used_memory,对于mem_fragmentation_ratio的值,存在如下的含义。

  • mem_fragmentation_ratio大于1时,表示Redis存在内存碎片,mem_fragmentation_ratio越大,内存碎片越多;
  • mem_fragmentation_ratio小于1时,表示Redis的部分内存交换到了磁盘(使用了虚拟内存),值越小,Redis交换到磁盘的内存就越多,此时Redis的速度就越慢。

正常情况下,mem_fragmentation_ratio的值需要大于1并控制在1.2以内。

Redis产生内存碎片是由于Redis的默认内存分配器jemalloc的内存分配机制导致的,jemalloc分配内存时,会按照如下规则。

  • 分配的内存空间满足2的幂次方;
  • 分配的内存空间满足大于等于需要分配的内存空间。

所以假如需要分配100byte,那么jemalloc会分配128byte的内存空间,此时Redis进程占用的这128byte的内存实际只有100byte被使用,未使用的28byte就是内存碎片。同时如果一个128byte的内存空间中只有部分数据被删除,那么这128byte的内存是不会被回收的,此时也产生了内存碎片。

四. Redis的淘汰策略

如果Redis使用的内存将超过maxmemory时,Redis会根据maxmemory_policy即淘汰策略来决定将哪些数据淘汰掉。Redis支持的淘汰策略如下表所示。

淘汰策略说明
noeviction默认策略。使用内存达到maxmemory时,如果添加新数据,不会删除任何旧数据,而是直接报错。
volatile-random对设置了过期时间(expire)的数据生效。随机删除一部分数据,直到有足够的内存空间分配给新数据,如果将设置了过期时间的数据全部删除完了都没有足够空间分配给新数据,此时报错。
volatile-ttl对设置了过期时间(expire)的数据生效。优先删除过期时间最小的数据,如果将设置了过期时间的数据全部删除完了都没有足够空间分配给新数据,此时报错。
volatile-lru对设置了过期时间(expire)的数据生效。使用LRU算法删除数据,如果将设置了过期时间的数据全部删除完了都没有足够空间分配给新数据,此时报错。
volatile-lfu对设置了过期时间(expire)的数据生效。使用LFU算法删除数据,如果将设置了过期时间的数据全部删除完了都没有足够空间分配给新数据,此时报错。
allkeys-random对所有数据生效。随机删除一部分数据,直到有足够的内存空间分配给新数据,如果没有数据可供删除且还未有足够空间分配给新数据,此时报错。
allkeys-lru对所有数据生效。使用LRU算法删除数据,如果没有数据可供删除且还未有足够空间分配给新数据,此时报错。
allkeys-lfu对所有数据生效。使用LFU算法删除数据,如果没有数据可供删除且还未有足够空间分配给新数据,此时报错。

上面提到的LRU(Least Recently Used)算法其实就是将最近被访问距离当前最久的数据删除,Redis会记录每个数据最后一次被访问的时间,在需要删除数据释放空间时,会根据每个数据最后一次被访问的时间选择出最“旧”的数据进行删除,所以LRU(Least Recently Used)算法有一个缺点,就是一个很少被访问但是最近被访问过的数据不会优先被删除,所以Redis4.0引入了LFU(Least Frequently Used)算法,即需要删除数据释放空间时,根据数据的访问频次筛选出最少被访问的数据进行删除,如果两条数据的访问频次相同,此时再根据数据最后一次被访问时间来决定删除哪条数据。

在第一节的例子中,可以看到Redis的淘汰策略是noeviction,所以在使用内存达到maxmemory后,Redis报了OOM的错误。

五. Redis的过期策略

上一节提到在Redis中可以使用expire指令为存储的数据设置过期时间,那么某条数据过期后,Redis会根据过期策略来删除这些过期数据,Redis中的过期策略如下所示。

过期策略说明
定时删除每个设置了过期时间的数据都有一个定时器,一旦数据过期,该数据会立即被删除。优点:过期数据可以及时被删除;缺点:过期数据多时定时器会占用较多CPU资源。
惰性删除使用数据的时候才去判断该数据是否过期,如果过期就删除该数据。优点:过期数据被访问时才会被删除,删除过期数据不会占用过多CPU资源;缺点:有些已经过期但是没有被访问的数据会长期得不到删除。
定期删除每隔一段时间扫描过期数据并删除。优点:通过控制扫描的间隔时间和执行时间,可以减少删除过期数据的操作对CPU资源的占用;缺点:扫描的间隔时间和执行时间难以确定一个合理值。

Redis中是采用惰性删除定期删除来处理过期数据的,同时Redis在进行定期删除时,是将设置了过期时间的key存放在字典中,默认情况下是每秒对字典进行10次扫描,每次扫描使用贪心策略遍历部分key来判断对应的数据是否过期,贪心策略如下。

  • 步骤1:从字典中随机选择20个key
  • 步骤2:删除这20个key中过期key对应的数据;
  • 步骤3:如果20个key中过期key占比超过25%,则重复步骤1-3。

总结

Redis将数据存放在内存中,所以了解Redis的内存机制对于使用Redis很有帮助。info memory指令能够列出Redis当前的内存状况和内存策略,可以通过maxmemory配置项来配置Redis的最大内存,也可以通过maxmemory-policy配置项来配置当内存到达最大值时的数据淘汰策略。Redis提供了共8种淘汰策略,可以仅针对设置了过期时间的数据生效,也可以针对所有数据生效,如果不配置淘汰策略,那么Redis采用的默认淘汰策略为不淘汰,此时内存达到最大值时Redis会报OOM错误。


半夏之沫
65 声望32 粉丝