【Redis】内存淘汰处理

首先根据内存逐出配置有下列几种内存淘汰策略:

# 内存逐出策略

# 当达到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;
        }
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

0x13

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值