内存回收策略


Redis的内存回收机制主要体现在以下两个方面:

  • 删除到达过期时间的键对象。
  • 内存使用达到maxmemory上限时触发内存溢出控制策略。

 

删除过期键对象


Redis所有的键都可以设置过期属性, 内部保存在过期字典中。 由于进程内保存大量的键, 维护每个键精准的过期删除机制会导致消耗大量的CPU, 对于单线程的Redis来说成本过高, 因此Redis采用惰性删除和定时任务删除机制实现过期键的内存回收。

  • 惰性删除: 惰性删除用于当客户端读取带有超时属性的键时, 如果已经超过键设置的过期时间, 会执行删除操作并返回空, 这种策略是出于节省CPU成本考虑, 不需要单独维护TTL链表来处理过期键的删除。但是单独用这种方式存在内存泄露的问题, 当过期键一直没有访问将无法得到及时删除, 从而导致内存不能及时释放。正因为如此, Redis还提供另一种定时任务删除机制作为惰性删除的补充。
  • 定时任务删除: Redis内部维护一个定时任务, 默认每秒运行10次(通过配置hz控制) 。 定时任务中删除过期键逻辑采用了自适应算法, 根据键的过期比例、 使用快慢两种速率模式回收键, 流程如图所示:

Redis 内存回收策略_内存溢出

流程说明:

  1. 定时任务在每个数据库空间随机检查20个键, 当发现过期时删除对应的键。
  2. 如果超过检查数25%的键过期, 循环执行回收逻辑直到不足25%或运行超时为止, 慢模式下超时时间为25毫秒。
  3. 如果之前回收键逻辑超时, 则在Redis触发内部事件之前再次以快模式运行回收过期键任务, 快模式下超时时间为1毫秒且2秒内只能运行1次。
  4. 快慢两种模式内部删除逻辑相同, 只是执行的超时时间不同。

 

内存溢出控制策略


当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制, Redis支持6种策略, 如下所示:
1) noeviction: 默认策略, 不会删除任何数据, 拒绝所有写入操作并返回客户端错误信息( error) OOM command not allowed when used memory, 此时Redis只响应读操作。
2) volatile-lru: 根据LRU算法删除设置了超时属性( expire) 的键, 直到腾出足够空间为止。 如果没有可删除的键对象, 回退到noeviction策略。
3) allkeys-lru: 根据LRU算法删除键, 不管数据有没有设置超时属性,直到腾出足够空间为止。

4) allkeys-random: 随机删除所有键, 直到腾出足够空间为止。
5) volatile-random: 随机删除过期键, 直到腾出足够空间为止。
6) volatile-ttl: 根据键值对象的ttl属性, 删除最近将要过期数据。 如果没有, 回退到noeviction策略。

内存溢出控制策略可以采用config set maxmemory-policy{policy}动态配置。 Redis支持丰富的内存溢出应对策略, 可以根据实际需求灵活定制, 比如当设置volatile-lru策略时, 保证具有过期属性的键可以根据LRU剔除, 而未设置超时的键可以永久保留。

还可以采用allkeys-lru策略把Redis变为纯缓存服务器使用。

当Redis因为内存溢出删除键时, 可以通过执行info stats命令查看evicted_keys指标找出当前Redis服务器已剔除的键数量。每次Redis执行命令时如果设置了maxmemory参数, 都会尝试执行回收内存操作。 当Redis一直工作在内存溢出(used_memory>maxmemory) 的状态下且设置非noeviction策略时, 会频繁地触发回收内存的操作, 影响Redis服务器的性能。 回收内存逻辑伪代码如下:

def freeMemoryIfNeeded() :
int mem_used, mem_tofree, mem_freed;
// 计算当前内存总量, 排除从节点输出缓冲区和AOF缓冲区的内存占用
int slaves = server.slaves;
mem_used = used_memory()-slave_output_buffer_size(slaves)-aof_rewrite_buffer_
size();
// 如果当前使用小于等于maxmemory退出
if (mem_used <= server.maxmemory) :
return REDIS_OK;
// 如果设置内存溢出策略为noeviction(不淘汰) , 返回错误。
if (server.maxmemory_policy == 'noeviction') :
return REDIS_ERR;
// 计算需要释放多少内存
mem_tofree = mem_used - server.maxmemory;
// 初始化已释放内存量
mem_freed = 0;
// 根据maxmemory-policy策略循环删除键释放内存
while (mem_freed < mem_tofree) :
// 迭代Redis所有数据库空间
for (int j = 0; j < server.dbnum; j++) :
String bestkey = null;
dict dict;
if (server.maxmemory_policy == 'allkeys-lru' ||
server.maxmemory_policy == 'allkeys-random'):
// 如果策略是 allkeys-lru/allkeys-random
// 回收内存目标为所有的数据库键
dict = server.db[j].dict;
else :
// 如果策略是volatile-lru/volatile-random/volatile-ttl
// 回收内存目标为带过期时间的数据库键
dict = server.db[j].expires;
// 如果使用的是随机策略, 那么从目标字典中随机选出键
if (server.maxmemory_policy == 'allkeys-random' ||
server.maxmemory_policy == 'volatile-random') :
// 随机返回被删除键
bestkey = get_random_key(dict);
else if (server.maxmemory_policy == 'allkeys-lru' ||
server.maxmemory_policy == 'volatile-lru') :
// 循环随机采样maxmemory_samples次(默认5次), 返回相对空闲时间最长的键
bestkey = get_lru_key(dict);
else if (server.maxmemory_policy == 'volatile-ttl') :
// 循环随机采样maxmemory_samples次, 返回最近将要过期的键
bestkey = get_ttl_key(dict);
// 删除被选中的键
if (bestkey != null) :
long delta = used_memory();
deleteKey(bestkey);
// 计算删除键所释放的内存量
delta -= used_memory();
mem_freed += delta;
// 删除操作同步给从节点
if (slaves):
flushSlavesOutputBuffers();
return REDIS_OK;

从伪代码可以看到, 频繁执行回收内存成本很高, 主要包括查找可回收键和删除键的开销, 如果当前Redis有从节点, 回收内存操作对应的删除命令会同步到从节点, 导致写放大的问题, 如图所示:

Redis 内存回收策略_redis_02

 

运维提示


建议线上Redis内存工作在maxmemory>used_memory状态下, 避免频繁内存回收开销。对于需要收缩Redis内存的场景, 可以通过调小maxmemory来实现快速回收。 比如对一个实际占用6GB内存的进程设置maxmemory=4GB, 之后第一次执行命令时, 如果使用非noeviction策略, 它会一次性回收到maxmemory指定的内存量, 从而达到快速回收内存的目的。 注意, 此操作会导致数据丢失和短暂的阻塞问题, 一般在缓存场景下使用。