redis源码之内存回收策略(五)

简介

redis可以配置最大使用内存,当使用内存超过配置的最大值的时候,需要主动进行内存回收。内存回收的两种基本策略:

  • lru: 淘汰最长时间没被访问的对象,这种策略需要维护对象被访问时间的信息。
  • lfu: 淘汰被访问次数最少的对象,这种策略需要维护对象被访问的频率信息。

redis内存淘汰基本策略

淘汰策略的配置

当redis内存达到使用限制时,可以配置如何选择key进行删除

# volatile表示在expire中进行驱逐, 所有设置了过期时间的key会关联到expire中
# allkeys表示在所有key中进行驱逐

# 如果要准确的淘汰就需要遍历整个字典或者expires字典,代价太高,所以redis是通过随机采样
# 然后排序,然后根据lru或者lfu策略选择删除最合适的key
# volatile-lru -> Evict using approximated LRU, only keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key having an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.

# The default is:
# 默认策略是不删除
# maxmemory-policy noeviction

# 默认是如果开启了lru或者lfu, 每采样5个key, 就删除一个key
# maxmemory-samples 5

lfu和lru信息的维护

key对应的val对象中存放了lfu和lru相关的信息,每次访问key都会更新val->lru

// get命令调用
lookupKeyReadWithFlags()  db.c
// set命令调用
lookupKeyWriteWithFlags() db.c

// db.c  查找key的函数
robj *lookupKey(redisDb *db, robj *key, int flags) {
    dictEntry *de = dictFind(db->dict,key->ptr);
    if (de) {
        // lru和lfu的相关信息是存放在val->lru中的
        robj *val = dictGetVal(de);

        /* Update the access time for the ageing algorithm.
         * Don't do it if we have a saving child, as this will trigger
         * a copy on write madness. */
        // LOOKUP_NOTOUCH表示不更新val的访问时间和次数
        if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
            // 如果设置了MAXMEMORY_FLAG_LFU, 也就是配置了达到最大内存的时
            // 候使用lfu删除key
            if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
                // 更新val的被访问次数信息
                updateLFU(val);
            } else {
                // 更新val的被访问时间为当前时间
                val->lru = LRU_CLOCK();
            }
        }
        return val;
    } else {
        return NULL;
    }
}

// db.c 更新lfu信息
void updateLFU(robj *val) {
    unsigned long counter = LFUDecrAndReturn(val);
    // 这里使用了随机数来概率性的触发增加counter,一种巧妙的设计
    counter = LFULogIncr(counter);
	   // lru的高24位存放访问时间,低8位存放访问次数,最大为255
    val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}

// server.h
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

内存回收触发机制

# 函数名  文件名
processCommand server.c
  # 检查是否达到了最大使用内存限制
  performEvictions evict.c
    # 随机采样key 并且删除最合适的key
    evictionPoolPopulate evict.c
	    # 异步回收,当开启了lazyfree-lazy-eviction且回收时间成本高于阈值
	    dbAsyncDelete lazyfree.c
		   dbSyncDelete db.c
  • 每次执行命令都要检查是否超过了最大使用内存,如果超过了就进行内存回收
  • 使用池(EvictionPoolLRU,一个数组)记录每次采样的key, 只有比池中的key更加适合删除的key才会被放入池,每次都从池中删除最合适的一个key;所以redis并不是简单的每次采样n个然后删除n个中的一个,而是累积了前面每次采样的最合适的key进行删除。(个人人为这里是不是可以使用一个小顶堆结构维护?如果key的idle时间比小顶堆的顶部大,就替换堆顶进行插入?采样n次后删除整个堆里面的key)

总结

  • redis可以使用LRU或者LFU策略进行内存回收
  • redis通过采样,并且将采样的结构放入一个数组内进行排序,然后删除最合适的key,每次采样maxmemory-samples个key, 删除一个key
posted @ 2024-09-27 17:00  董少奇  阅读(39)  评论(0)    收藏  举报