Redis中的内存释放与过期键删除

Redis通过内存释放策略和过期键删除策略来防止内存饱和。内存释放包括volatile-lru、volatile-ttl等六种策略,而过期键删除分为惰性删除和定期删除。惰性删除在访问键时检查是否过期,定期删除则在serverCron函数执行时主动删除过期键。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

在Redis中,内存的大小是有限的,所以为了防止内存饱和,需要实现某种键淘汰策略。主要有两种方法,一种是当Redis内存不足时所采用的内存释放策略。另一种是对过期键进行删除的策略,也可以在某种程度上释放内存。

相关数据结构

Redis中的数据库结构如下:

/*
 * 数据库结构
 */
typedef struct redisDb {
    // key space,包括键值对象
    dict *dict;                 /* The keyspace for this DB */
    // 保存 key 的过期时间
    dict *expires;              /* Timeout of keys with a timeout set */
    // 正因为某个/某些 key 而被阻塞的客户端
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */
    // 某个/某些接收到 PUSH 命令的阻塞 key
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    // 正在监视某个/某些 key 的所有客户端
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    // 数据库的号码
    int id;
} redisDb;

其中的expires字典保存了数据库总所有键的过期时间。在expires里,对象中的键和dict一样,但是它的value是标识过期时间的值,以便在删除过期键的时候使用。

Redis的服务器中有一个为LRU算法准备的lruclock:

struct redisServer{
       ....
       /* Clock incrementing every minute, for LRU */
       unsigned lruclock:22;
       ....
}

这个lruclock会在定时调用的函数serverCron中进行实时更新。而创建对象的时候,会将对象的lru设置成当前的服务器的lruclock。同样,在访问键的时候,会对lru进行一次更新。

/*
 * 根据给定类型和值,创建新对象
 */
robj *createObject(int type, void *ptr) {

    // 分配空间
    robj *o = zmalloc(sizeof(*o));
    ......
    /* Set the LRU to the current lruclock (minutes resolution). */
    o->lru = server.lruclock;

    return o;
}

内存释放的策略

Redis中有专门释放内存的函数:freeMmoryIfNeeded。每当执行一个命令的时候,就会调用该函数来检测内存是否够用。如果已用内存大于最大内存限制,它就会进行内存释放。

    /* Check if we are over the memory limit. */
    if (mem_used <= server.maxmemory) return REDIS_OK;

    if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
        return REDIS_ERR; /* We need to free memory, but policy forbids. */

当需要进行内存释放的时候,需要用某种策略对保存的的对象进行删除。Redis有六种策略:

  1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-enviction(驱逐):禁止驱逐数据

先判断是从过期集expires中删除键还是从所有数据集dict中删除键

            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
            {
                dict = server.db[j].dict;
            } else {
                dict = server.db[j].expires;
            }
如果是随机算法,就直接挑选一个随机键进行删除

            /* volatile-random and allkeys-random policy */
            // 随机算法
            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
            {
                de = dictGetRandomKey(dict);
                bestkey = dictGetKey(de);
            }

如果是LRU算法,就采用局部的LRU。意思是不是从所有数据中找到LRU,而是随机找到若干个键,删除其中的LRU键。

            /* volatile-lru and allkeys-lru policy */
            // LRU 算法
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
            {
                for (k = 0; k < server.maxmemory_samples; k++) {
                    sds thiskey;
                    long thisval;
                    robj *o;

                    de = dictGetRandomKey(dict);
                    thiskey = dictGetKey(de);
                    /* When policy is volatile-lru we need an additional lookup
                     * to locate the real key, as dict is set to db->expires. */
                    if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
                        de = dictFind(db->dict, thiskey);
                    o = dictGetVal(de);
                    thisval = estimateObjectIdleTime(o);

                    /* Higher idle time is better candidate for deletion */
                    if (bestkey == NULL || thisval > bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }
            }
如果是TTL算法,就在expires中随机挑几个数据,找到最近的要过期的键进行删除。

            /* volatile-ttl */
            // TTL 算法
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
                for (k = 0; k < server.maxmemory_samples; k++) {
                    sds thiskey;
                    long thisval;

                    de = dictGetRandomKey(dict);
                    thiskey = dictGetKey(de);
                    thisval = (long) dictGetVal(de);

                    /* Expire sooner (minor expire unix timestamp) is better
                     * candidate for deletion */
                    if (bestkey == NULL || thisval < bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }
            }

过期键删除的策略

惰性删除

所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查。如果过期就删除,如果没过期就正常访问。这种只有在访问时才检查过期的策略叫做定期删除。

int expireIfNeeded(redisDb *db, robj *key) {
    // 取出 key 的过期时间
    long long when = getExpire(db,key);

    // key 没有过期时间,直接返回
    if (when < 0) return 0; /* No expire for this key */

    /* Don't expire anything while loading. It will be done later. */
    // 不要在服务器载入数据时执行过期
    if (server.loading) return 0;

    /* If we are running in the context of a slave, return ASAP:
     * the slave key expiration is controlled by the master that will
     * send us synthesized DEL operations for expired keys.
     *
     * Still we try to return the right information to the caller, 
     * that is, 0 if we think the key should be still valid, 1 if
     * we think the key is expired at this time. */
    // 如果服务器作为附属节点运行,那么直接返回
    // 因为附属节点的过期是由主节点通过发送 DEL 命令来删除的
    // 不必自主删除
    if (server.masterhost != NULL) {
        // 返回一个理论上正确的值,但不执行实际的删除操作
        return mstime() > when;
    }

    /* Return when this key has not expired */
    // 未过期
    if (mstime() <= when) return 0;

    /* Delete the key */
    server.stat_expiredkeys++;

    // 传播过期命令
    propagateExpire(db,key);

    // 从数据库中删除 key
    return dbDelete(db,key);
}

定期删除

每当周期性函数serverCron执行时,会调用activeExpireCycle进行主动的过期键删除。具体方法是在规定的时间内,多次从expires中随机挑一个键,检查它是否过期,如果过期则删除。

void activeExpireCycle(void) {
    int j, iteration = 0;
    long long start = ustime(), timelimit;

    /* We can use at max REDIS_EXPIRELOOKUPS_TIME_PERC percentage of CPU time
     * per iteration. Since this function gets called with a frequency of
     * REDIS_HZ times per second, the following is the max amount of
     * microseconds we can spend in this function. */
    // 这个函数可以使用的时长(毫秒)
    timelimit = 1000000*REDIS_EXPIRELOOKUPS_TIME_PERC/REDIS_HZ/100;
    if (timelimit <= 0) timelimit = 1;

    for (j = 0; j < server.dbnum; j++) {
        int expired;
        redisDb *db = server.db+j;

        /* Continue to expire if at the end of the cycle more than 25%
         * of the keys were expired. */
        do {
            unsigned long num = dictSize(db->expires);
            unsigned long slots = dictSlots(db->expires);
            long long now = mstime();

            /* When there are less than 1% filled slots getting random
             * keys is expensive, so stop here waiting for better times...
             * The dictionary will be resized asap. */
            // 过期字典里只有 %1 位置被占用,调用随机 key 的消耗比较高
            // 等 key 多一点再来
            if (num && slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;

            /* The main collection cycle. Sample random keys among keys
             * with an expire set, checking for expired ones. */
            // 从过期字典中随机取出 key ,检查它是否过期
            expired = 0;    // 被删除 key 计数
            if (num > REDIS_EXPIRELOOKUPS_PER_CRON) // 最多每次可查找的次数
                num = REDIS_EXPIRELOOKUPS_PER_CRON;
            while (num--) {
                dictEntry *de;
                long long t;

                // 随机查找带有 TTL 的 key ,看它是否过期
                // 如果数据库为空,跳出
                if ((de = dictGetRandomKey(db->expires)) == NULL) break;

                t = dictGetSignedIntegerVal(de);
                if (now > t) {
                    // 已过期
                    sds key = dictGetKey(de);
                    robj *keyobj = createStringObject(key,sdslen(key));

                    propagateExpire(db,keyobj);
                    dbDelete(db,keyobj);
                    decrRefCount(keyobj);
                    expired++;
                    server.stat_expiredkeys++;
                }
            }
            /* We can't block forever here even if there are many keys to
             * expire. So after a given amount of milliseconds return to the
             * caller waiting for the other active expire cycle. */
            // 每次进行 16 次循环之后,检查时间是否超过,如果超过,则退出
            iteration++;
            if ((iteration & 0xf) == 0 && /* check once every 16 cycles. */
                (ustime()-start) > timelimit) return;

        } while (expired > REDIS_EXPIRELOOKUPS_PER_CRON/4);
    }
}


评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值