Redis内存回收机制,把我整懵了...
之前看到过一道面试题:Redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现?
图片来自 Pexels
笔者结合在工作上遇到的问题学习分析,希望看完这篇文章能对大家有所帮助。
从一次不可描述的故障说起
问题描述:一个依赖于定时器任务的生成的接口列表数据,时而有,时而没有。
怀疑是 Redis 过期删除策略
排查过程长,因为手动执行定时器,Set 数据没有报错,但是 Set 数据之后不生效。
Set 没报错,但是 Set 完再查的情况下没数据,开始怀疑 Redis 的过期删除策略(准确来说应该是 Redis 的内存回收机制中的数据淘汰策略触发内存上限淘汰数据),导致新加入 Redis 的数据都被丢弃了。
最终发现故障的原因是因为配置错了,导致数据写错地方,并不是 Redis 的内存回收机制引起。
通过这次故障后思考总结,如果下一次遇到类似的问题,在怀疑 Redis 的内存回收之后,如何有效地证明它的正确性?如何快速证明猜测的正确与否?以及什么情况下怀疑内存回收才是合理的呢?
下一次如果再次遇到类似问题,就能够更快更准地定位问题的原因。另外,Redis 的内存回收机制原理也需要掌握,明白是什么,为什么。
花了点时间查阅资料研究 Redis 的内存回收机制,并阅读了内存回收的实现代码,通过代码结合理论,给大家分享一下 Redis 的内存回收机制。
为什么需要内存回收?
原因有如下两点:
在 Redis 中,Set 指令可以指定 Key 的过期时间,当过期时间到达以后,Key 就失效了。
Redis 是基于内存操作的,所有的数据都是保存在内存中,一台机器的内存是有限且很宝贵的。
Redis 的内存回收机制
过期删除策略
①定时删除
②惰性删除
③定期删除
过期删除策略原理
①RedisDB 结构体定义
typedef struct redisDb {
dict *dict; /* 数据库的键空间,保存数据库中的所有键值对 */
dict *expires; /* 保存所有过期的键 */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* 数据库ID字段,代表不同的数据库 */
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;
从结构定义中我们可以发现,对于每一个 Redis 数据库,都会使用一个字典的数据结构来保存每一个键值对,dict 的结构图如下:
②expires 属性
Set 指定过期时间 expire,如果设置 Key 的时候指定了过期时间,Redis 会将这个 Key 直接加入到 expires 字典中,并将超时时间设置到该字典元素。
调用 expire 命令,显式指定某个 Key 的过期时间。
恢复或修改数据,从 Redis 持久化文件中恢复文件或者修改 Key,如果数据中的 Key 已经设置了过期时间,就将这个 Key 加入到 expires 字典中。
③Redis 清理过期 Key 的时机
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData)
{
…
/* Handle background operations on Redis databases. */
databasesCron();
...
}
robj *lookupKeyRead(redisDb *db, robj *key) {
robj *val;
expireIfNeeded(db,key);
val = lookupKey(db,key);
...
return val;
}
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
void beforeSleep(struct aeEventLoop *eventLoop) {
...
/* Run a fast expire cycle (the called function will return
- ASAP if a fast cycle is not needed). */
if (server.active_expire_enabled && server.masterhost == NULL)
activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST);
...
}
④过期策略的实现
server.hz 配置了 serverCron 任务的执行周期,默认是 10,即 CPU 空闲时每秒执行十次。
每次清理过期 Key 的时间不能超过 CPU 时间的 25%:timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100。
比如,如果 hz=1,一次清理的最大时间为 250ms,hz=10,一次清理的最大时间为 25ms。
如果是快速清理模式(在 beforeSleep 函数调用),则一次清理的最大时间是 1ms。
依次遍历所有的 DB。
从 DB 的过期列表中随机取 20 个 Key,判断是否过期,如果过期,则清理。
如果有 5 个以上的 Key 过期,则重复步骤 5,否则继续处理下一个 DB。
在清理过程中,如果达到 CPU 的 25% 时间,退出清理过程。
⑤删除 Key
小结:总的来说,Redis 的过期删除策略是在启动时注册了 serverCron 函数,每一个时间时钟周期,都会抽取 expires 字典中的部分 Key 进行清理,从而实现定期删除。
另外,Redis 会在访问 Key 时判断 Key 是否过期,如果过期了,就删除,以及每一次 Redis 访问事件到来时,beforeSleep 都会调用 activeExpireCycle 函数,在 1ms 时间内主动清理部分 Key,这是惰性删除的实现。
内存淘汰策略
Redis 的内存淘汰机制如下:
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间(server.db[i].dict)中,移除最近最少使用的 Key(这个是最常用的)。
allkeys-random:当内存不足以容纳新写入数据时,在键空间(server.db[i].dict)中,随机移除某个 Key。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].expires)中,移除最近最少使用的 Key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].expires)中,随机移除某个 Key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].expires)中,有更早过期时间的 Key 优先移除。
在配置文件中,通过 maxmemory-policy 可以配置要使用哪一个淘汰机制。
①什么时候会进行淘汰?
Redis 会在每一次处理命令的时候(processCommand 函数调用 freeMemoryIfNeeded)判断当前 Redis 是否达到了内存的最大限制,如果达到限制,则使用对应的算法去处理需要删除的 Key。
int processCommand(client *c)
{
...
if (server.maxmemory) {
int retval = freeMemoryIfNeeded();
}
...
}
②LRU 实现原理
keys = getSomeKeys(dict, sample)
key = findSmallestIdle(keys)
remove(key)
回到问题原点
设置的过期时间过短,比如,1s。
内存超过了最大限制,且设置的是 noeviction 或者 allkeys-random。
如果内存没有超,或者内存淘汰算法不是上面的两者,则还需要看看 Key 是否已经过期,通过 TTL 查看 Key 的存活时间。
总结
每一次访问的时候判断 Key 的过期时间是否到达,如果到达,就删除 Key。
Redis 启动时会创建一个定时事件,会定期清理部分过期的 Key,默认是每秒执行十次检查,每次过期 Key 清理的时间不超过 CPU 时间的 25%。
即若 hz=1,则一次清理时间最大为 250ms,若 hz=10,则一次清理时间最大为 25ms。
对于第二种情况,Redis 会在每次处理 Redis 命令的时候判断当前 Redis 是否达到了内存的最大限制,如果达到限制,则使用对应的算法去处理需要删除的 Key。
编辑:陶家龙、孙淑娟
精彩文章推荐: