首先根据内存逐出配置有下列几种内存淘汰策略:
# 内存逐出策略
# 当达到MAXMEMORY时,Redis将如何选择要删除的内容。您可以在八种行为中进行选择:
# 1.从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 volatile-lru
# 2.从数据集中挑选最近最少使用的数据淘汰 allkeys-lru
# 3.从已设置过期时间的数据集挑选使用频率最低的数据淘汰 volatile-lfu
# 4.从数据集中挑选使用频率最低的数据淘汰 allkeys-lfu
# 5.从已设置过期时间的数据集中任意选择数据淘汰 volatile-random
# 6.从数据集中任意选择数据淘汰 allkeys-random
# 7.从已设置过期时间的数据集中挑选将要过期的数据淘汰 volatile-ttl
# 8.禁止驱逐数据,这也是默认策略 noeviction
maxmemory-policy noeviction
# 设置内存最大限制
maxmemory <bytes>
lru 算法分析:
首先 lru 算法需要维护一个链表,每次对一个key进行更新、方位、插入之后需要将这个key从链表的中间位置移动到头部。整个链表从头部到尾部按照使用顺序排列,越靠前越不应该淘汰。
在 redis 中为了节省空间以及提高性能没有使用链表来维护这个lru链表,而是在每次操作一个key时更新这个key的操作时间,按照操作时间排序来代替lru的链表维护操作所以是近视 lru 算法。
首先需要一个全局时钟,每次操作key时需要从时钟获取一个时间戳:
全局时钟头文件头文件:
#define LRU_BITS 24 /* 定义 redisObject 中 lru 字段的位数 */
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* 定义 redisObject 中 lru 字段最大值 */
#define LRU_CLOCK_RESOLUTION 1000 /* LRU 时钟精度 ms */
redisObject 结构体:
typedef struct redisObject {
unsigned type:4; /* 对象类型 REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)*/
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time 这个字段用来排序,记录这个对象最后一次访问时间*/
int refcount; /* 引用计数,可用作访问频率判断 */
void *ptr;
} robj;
这个 redisObject 对象,在一个 key 被创建时进行初始化:
server.c processCommand() => 指令处理
每个 key 在 set、update等操作后会更新这个 key 对应 redisObject 对象里面的 lru 字段(Update the access time for the ageing algorithm):
【log】t_string.c setCommand() => 指令 set 处理
【log】t_string.c setGenericCommand() =>
【log】db.c lookupKeyWrite() =>
【log】db.c lookupKeyWriteWithFlags() =>
【log】db.c lookupKey() 更新 lur 时间
robj *lookupKey(redisDb *db, robj *key, int flags) {
dictEntry *de = dictFind(db->dict,key->ptr);
if (de) {
// 查询得到的 redisObject
robj *val = dictGetVal(de);
// Update the access time for the ageing algorithm
if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
updateLFU(val);
} else {
val->lru = LRU_CLOCK();
}
}
return val;
} else {
return NULL;
}
}
内存淘汰的触发是在处理客户端指令的时候,如果此时内存达到限值且配置了某种淘汰策略时才进行淘汰,所以如果在达到限制时持续大量请求会造成相互阻塞性能急剧下降。
入口函数是调用顺序如下:
server.c processCommand() 代码如下:
int processCommand(client *c) {
// 判断是否打开内存淘汰机制
if (server.maxmemory && !server.lua_timedout) {
int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;
}
}
触发淘汰的条件是 server.maxmemory !=0,默认配置的 noeviction(不淘汰)策略下,这个字段是 0,由系统初始化时设置进去。如果开启了内存淘汰,配置了最大内存值,会在系统初始化时进行设置:
【log】server.c initServer() 服务初始化
【log】server.c printServerInfo() 输出 redisServer结构体
【log】server.maxmemory=10240
然后调用内存淘汰函数:
evict.c freeMemoryIfNeededAndSafe() => 内存淘汰入口
evict.c freeMemoryIfNeeded() => 内存淘汰开始
内存淘汰函数分析如下,会判断内存是否达到限制的,并j计算需要释放多少空间到 mem_tofree 指针中,然后循环删除:
evict.c freeMemoryIfNeeded()
int freeMemoryIfNeeded(void) {
int keys_freed = 0;
if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;
size_t mem_reported, mem_tofree, mem_freed;
mstime_t latency, eviction_latency, lazyfree_latency;
long long delta;
int slaves = listLength(server.slaves);
int result = C_ERR;
if (clientsArePaused()) return C_OK;
// 判断内存是否到达到限制,返回 C_OK 或者 C_ERR,总使用内存保存在 mem_reported 指针中,需要释放的内存大小保存在 mem_tofree 中
if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)
return C_OK;
// 走到这里说明需要执行内存淘汰了
mem_freed = 0;
latencyStartMonitor(latency);
// 如果策略为 noeviction 不淘汰则直接跳过
if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
goto cant_free; /* We need to free memory, but policy forbids. */
// 循环按照策略进行删除,直到删除 mem_tofree 大小
while (mem_freed < mem_tofree) {
// 省略
}
}
该函数 while 循环体中,会根据不同的淘汰策略进行删除,判断 maxmemory_policy 不同策略执行不同 if 分支计算需要删除的 key,最后进行删除:
// 循环按照策略进行删除,直到删除 mem_tofree 大小
while (mem_freed < mem_tofree) {
// MAXMEMORY_FLAG_LRU 设置了过期的key中挑选最近最少使用,评估时间,删除很长时间未使用
// MAXMEMORY_FLAG_LFU 设置了过期的key中挑选最近最少使用,评估时间和使用次数,删除一段时间内使用频率最低
// MAXMEMORY_VOLATILE_TTL 设置了过期的key中挑选最快过期的一个key
if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL){ //省略 }
// MAXMEMORY_ALLKEYS_RANDOM 从所有key中随机挑选
// MAXMEMORY_VOLATILE_RANDOM 从所有key中挑选最近最少使用的一个key
else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM || server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM){ //省略 }
// 如果前面计算出某个需要删除的key
if (bestkey) { //省略 }
}
以第一个分支也就是使用最多的 volatile-lru 策略分析,系统构造一个 evictionPoolEntry 对象集合来保存需要逐出的 key ,对每个 db 调用 evictionPoolPopulate() 函数去查找可能需要驱逐的一批 key 塞到这个 evictionPoolEntry 中:
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;
/* We don't want to make local-db choices when expiring keys,
* so to start populate the eviction pool sampling keys from
* every DB. */
for (i = 0; i < server.dbnum; i++) {
db = server.db+i;
dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
db->dict : db->expires;
if ((keys = dictSize(dict)) != 0) {
// 从每个db中
evictionPoolPopulate(i, dict, db->dict, pool);
total_keys += keys;
}
}
if (!total_keys) break; /* No keys to evict. */
/* Go backward from best to worst element to evict. */
for (k = EVPOOL_SIZE-1; k >= 0; k--) {
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);
}
/* Remove the entry from the pool. */
if (pool[k].key != pool[k].cached)
sdsfree(pool[k].key);
pool[k].key = NULL;
pool[k].idle = 0;
/* If the key exists, is our pick. Otherwise it is
* a ghost and we need to try the next element. */
if (de) {
bestkey = dictGetKey(de);
break;
} else {
/* Ghost... Iterate again. */
}
}
}
}
evictionPoolPopulate() 函数的作用是从一个 db 中遍历并计算每个 key 的 redisObject -> lru 值,以升序插入到 evictionPoolEntry 暂存池中:
void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
int j, k, count;
dictEntry *samples[server.maxmemory_samples];
// 从db中取一定数量的key
count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
for (j = 0; j < count; j++) {
unsigned long long idle;
sds key;
robj *o;
dictEntry *de;
de = samples[j];
// key 的名称
key = dictGetKey(de);
if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
if (sampledict != keydict) de = dictFind(keydict, key);
// 取 key 对应的 redisObject 对象
o = dictGetVal(de);
}
if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
// 计算这个 key 的 idel 时长, 参数就是 redisObject 对象
idle = estimateObjectIdleTime(o);
}
// 省略
k = 0;
// 插入到 pool 暂存池
while (k < EVPOOL_SIZE &&pool[k].key &&pool[k].idle < idle) k++;
// 省略
}
}
最终判断 bestKey,判断是否选择出最需要进行删除的key,则进行删除,代码如下:
// 如果前面计算出某个需要删除的key
if (bestkey) {
db = server.db+bestdbid;
robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
delta = (long long) zmalloc_used_memory();
latencyStartMonitor(eviction_latency);
// 延迟删除
if (server.lazyfree_lazy_eviction)
dbAsyncDelete(db,keyobj);
else
dbSyncDelete(db,keyobj);
latencyEndMonitor(eviction_latency);
latencyAddSampleIfNeeded("eviction-del",eviction_latency);
delta -= (long long) zmalloc_used_memory();
mem_freed += delta;
server.stat_evictedkeys++;
signalModifiedKey(NULL,db,keyobj);
notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
keyobj, db->id);
decrRefCount(keyobj);
keys_freed++;
if (slaves) flushSlavesOutputBuffers();
if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
/* Let's satisfy our stop condition. */
mem_freed = mem_tofree;
}
}
}