在上一篇文章中,介绍了一下 Redis LRU 内存淘汰、LFU 内存淘汰以及两种算法的区别:Redis 之 LRU 与 LFU 可以知道,LRU我们可以配置六种策略,LFU 同样可以配置,那么今天就来看看源码, Redis 是怎样的一个逻辑来进行内存淘汰的
算是第一篇解析源码的文章,一边看着源码注释一边自己理解写下来的,当中有什么不对或者改进的,还希望留言指教
/*
* 1、内存淘汰调用的方法是 freeMemoryIfNeededAndSafe(),实际调用如下方法
* 这个方法会定期的被调用,以便于查看根据当前设置的 maxmemory 来去确定释放多少内存,假如我们的应用程序超过了这
* 个设置的内存限制,这个方法将会尝试释放内存以达到内存限制以下
*
* 2、当我们的应用程序使用的内存低于当前服务设置的内存限制以下,或者曾经在内存限制以上,但是后来尝试释放内存成功
* 就会返回 C_OK
* 否则当占用的内存在内存限制以上,但是又没有足够的内存可以被释放到内存限制以下,方法就会返回 C_ERR
*
* 3、在 redis 源码的 server.h 文件中,定义如下
* C_OK 0
* C_ERR -1
*/
int freeMemoryIfNeeded(void) { (1)
//默认情况下,只有主节点需要来释放内存,
if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK; (2)
size_t mem_reported, mem_tofree, mem_freed;
mstime_t latency, eviction_latency;
long long delta;
//所有的从节点数
int slaves = listLength(server.slaves); (3)
if (clientsArePaused()) return C_OK; (4)
/*
* 1、获取内存用量状态,如果在设置的内存限制以下,返回 C_OK,不需要释放内存
* 2、getMaxmemoryState 会计算总共使用了多少内存,需要释放多少内存
*/
if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK) (5)
return C_OK;
//初始化释放的内存数值,用来统计释放了多少内存
mem_freed = 0;
// 当前设置的是不进行内存淘汰,goto 跳转处理逻辑
if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION) (6)
goto cant_free;
//延迟开启监控
latencyStartMonitor(latency); (7)
//尝试释放足够大的内存
while (mem_freed < mem_tofree) { (8)
int j, k, i, keys_freed = 0;
static unsigned int next_db = 0;
sds bestkey = NULL;
int bestdbid;
redisDb *db;
dict *dict;
dictEntry *de;
//
if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
{
struct evictionPoolEntry *pool = EvictionPoolLRU;
while(bestkey == NULL) {
unsigned long total_keys = 0, keys;
/*
* 获取所有需要被淘汰的 key
*/
for (i = 0; i < server.dbnum; i++) { (9)
db = server.db+i;
dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
db->dict : db->expires;
if ((keys = dictSize(dict)) != 0) {
/*
* 这是freememoryifrequired()的一个帮助函数,以便于在每次过期一个 key 的时候在 evictionPool 里填充一个键值对空间
* 空闲时间小于当前键里面任何一个键,这个 key 就会被添加。
* 按照升序的方式插入 key, 闲置时间越短的在左边,长的在右边
* /
evictionPoolPopulate(i, dict, db->dict, pool);
total_keys += keys;
}
}
/*
* 没有需要被淘汰的 Key
*/
if (!total_keys) break; (10)
//遍历数据库 ,从所有的 key 里获取到字典键值对,或者从过期的 key 里面获取
for (k = EVPOOL_SIZE-1; k >= 0; k--) { (11)
if (pool[k].key == NULL) continue;
bestdbid = pool[k].dbid;
if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
de = dictFind(server.db[pool[k].dbid].dict,
pool[k].key);
} else {
de = dictFind(server.db[pool[k].dbid].expires,
pool[k].key);
}
//从 pool 里移除键值对
if (pool[k].key != pool[k].cached) (12)
sdsfree(pool[k].key);
pool[k].key = NULL;
pool[k].idle = 0;
//如果存在就获取到最需要淘汰的 key,否则需要尝试下一个元素来判断
if (de) { (13)
bestkey = dictGetKey(de);
break;
} else {
(14)
}
}
}
}
//如果淘汰策略是 allkeys_random 以及 volatile_random
else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM || (15)
server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
{
for (i = 0; i < server.dbnum; i++) { (16)
j = (++next_db) % server.dbnum;
db = server.db+j;
dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
db->dict : db->expires;
if (dictSize(dict) != 0) {
de = dictGetRandomKey(dict);
bestkey = dictGetKey(de);
bestdbid = j;
break;
}
}
}
//最后移除被选出来的 key
if (bestkey) { (17)
db = server.db+bestdbid;
robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
//主节点 key 过期时,会发送这个 key 的删除指令到所有的从节点以及 aof 文件,如果启用了主从的话
propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
//获取当前以及占用的内存空间大小,因为 AOF 和输出缓冲区最后都会被释放,因此我们仅仅只要关心 key 占用的空间就好了
delta = (long long) zmalloc_used_memory(); (18)
latencyStartMonitor(eviction_latency);
//如果开启了惰性释放,就会对这个 key 对象做异步的删除,否则就是同步
if (server.lazyfree_lazy_eviction)
dbAsyncDelete(db,keyobj);
else
dbSyncDelete(db,keyobj);
//结束延迟监控
latencyEndMonitor(eviction_latency);
//延迟抽样采集
latencyAddSampleIfNeeded("eviction-del",eviction_latency);
latencyRemoveNestedEvent(latency,eviction_latency);
delta -= (long long) zmalloc_used_memory();
//统计释放的内存,就是用之前统计的那次内存数值减去现在的就是刚才释放的
mem_freed += delta;
server.stat_evictedkeys++;
//发布一个 key 进行内存淘汰的事件
notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
keyobj, db->id);
//更新引用计数器
decrRefCount(keyobj);
keys_freed++;
/*
* 当要释放的内存足够大的时候,可能会花很多时间在这个地方,导致延迟,以至于不可能足够快的将数据传输到从节点,所以我们强制
* 在循环内传输同步数据
*/
if (slaves) flushSlavesOutputBuffers(); (19)
/*
* 如果是异步删除,需要在循环过程中定期去看后台是否释放了足够的内存,默认每 16 个 Key 循环检查一次
* 一般来说停止的条件就是能够固定的释放,能够提前计算内存的总数,然而当另外一个线程正在删除对象 key 的时候,最好检查下,因为
* 有时候如果已经到了目标的内存大小值,但是 mem_freed 这个值却是只在 dbAsyncDelete 中计算的,与此同时一直有线程在释放内存
* 就会导致计算出错
* /
if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) { (20)
if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
//满足停止的条件
mem_freed = mem_tofree; (21)
}
}
}
//没有 key 被释放,结束延迟监控,处理对应逻辑 (22)
if (!keys_freed) {
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("eviction-cycle",latency);
goto cant_free; /* nothing to free... */
}
}
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("eviction-cycle",latency);
return C_OK;
/*
* 无法释放内存时,阻塞,检查一下后台清理线程是否还有任务正在清理,等他清理出足够内存之后
* 再退出
*
*/
cant_free: (23)
while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
break;
usleep(1000);
}
return C_ERR;
}
沉下心来,细细的查看源代码,可以让我们发现很多平时我们工作中根本察觉不到的底层变化,能够更好的使用