Redis 6.2的过期删除策略源码分析(惰性删除)

都知道redis采用的过期删除策略是定期删除惰性删除,对于这l两个的解释可以看一下Redis 键的过期删除策略及缓存淘汰策略
下面是根据翻译软件和自己的理解翻译过来的,英文原文也在上面,如果不清楚或者不对可以看一下英文
定时删除的源码可以看一下我的另一篇文章Redis的过期删除策略源码分析(定时删除)

1、当查询key的源码执行逻辑

你会疑问为什么标题明明是惰性删除策略,而这里是查询key的执行的逻辑呢?
如果你看过上面的链接你就不会这么问了,那是因为惰性删除策略只有你在查询或者操作时才执行key是否过期,所以看下去就对了

/* Lookup a key for read operations, or return NULL if the key is not found
 * in the specified DB.
 * 为读取操作查找键,如果在指定的数据库中找不到该键,则返回NULL
 * As a side effect of calling this function:
 * 1. A key gets expired if it reached it's TTL.
 * 2. The key last access time is updated.
 * 3. The global keys hits/misses stats are updated (reported in INFO).
 * 4. If keyspace notifications are enabled, a "keymiss" notification is fired.
 * 作为调用此函数的副作用:
 * 1.如果一个密钥到达了它的TTL,它就会过期。
 * 2.更新密钥上次访问时间。
 * 3.更新全局键命中/未命中统计(在INFO中报告)。
 * 4.如果启用了键空间通知,则会触发“keymiss”通知。
 * This API should not be used when we write to the key after obtaining
 * the object linked to the key, but only for read only operations.
 *  当我们在获取链接到键的对象后写入键时,不应使用此API,而应仅用于只读操作
 * Flags change the behavior of this command:
 *  标志更改此命令的行为
 *  LOOKUP_NONE (or zero): no special flags are passed.
 * LOOKUP_NONE (or zero):不传递特殊标志。
 *  LOOKUP_NOTOUCH: don't alter the last access time of the key.
 *  LOOKUP_NOTOUCH: 不要更改key的最后访问时间
 * Note: this function also returns NULL if the key is logically expired
 * but still existing, in case this is a slave, since this API is called only
 * for read operations. Even if the key expiry is master-driven, we can
 * correctly report a key is expired on slaves even if the master is lagging
 * expiring our key via DELs in the replication link.
 *注意:如果密钥在逻辑上过期,此函数还返回NULL
 *但仍然存在,以防这是一个从属,因为这个API只被调用
 *用于读取操作。即使key过期是主驱动的,我们也可以
 *正确报告从机上的key过期,即使主机延迟
 *通过复制链接中的DELs使密钥过期
 */
robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
    robj *val;

    if (expireIfNeeded(db,key) == 1) { //这个方法是关键点
        /* If we are in the context of a master, expireIfNeeded() returns 1
         * when the key is no longer valid, so we can return NULL ASAP. */
        if (server.masterhost == NULL)
            goto keymiss;

        /* However if we are in the context of a slave, expireIfNeeded() will
         * not really try to expire the key, it only returns information
         * about the "logical" status of the key: key expiring is up to the
         * master in order to have a consistent view of master's data set.
         *
         * However, if the command caller is not the master, and as additional
         * safety measure, the command invoked is a read-only command, we can
         * safely return NULL here, and provide a more consistent behavior
         * to clients accessing expired values in a read-only fashion, that
         * will say the key as non existing.
         *
         * Notably this covers GETs when slaves are used to scale reads. */
        if (server.current_client &&
            server.current_client != server.master &&
            server.current_client->cmd &&
            server.current_client->cmd->flags & CMD_READONLY)
        {
            goto keymiss;
        }
    }
    val = lookupKey(db,key,flags);
    if (val == NULL)
        goto keymiss;
    server.stat_keyspace_hits++;
    return val;

keymiss:
    if (!(flags & LOOKUP_NONOTIFY)) {
        notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
    }
    server.stat_keyspace_misses++;
    return NULL;
}

这里有一个关键的方法就是expireIfNeeded,这个是判断这个key是否过期的方法

2、expireIfNeeded(判断key是否过期)

/* This function is called when we are going to perform some operation
 * in a given key, but such key may be already logically expired even if
 * it still exists in the database. The main way this function is called
 * is via lookupKey*() family of functions.
 *当我们要在给定的key中执行某些操作时,会调用此函数,因为这样的key可能已经在逻辑上过期,即使它仍然存在于数据库中。
 *调用此函数的主要方式是通过lookupKey*()函数族
 *
 * The behavior of the function depends on the replication role of the
 * instance, because slave instances do not expire keys, they wait
 * for DELs from the master for consistency matters. However even
 * slaves will try to have a coherent return value for the function,
 * so that read commands executed in the slave side will be able to
 * behave like if the key is expired even if still present (because the
 * master has yet to propagate the DEL).
 *函数的行为取决于实例的复制角色,因为从属实例不会使密钥过期,所以它们会等待来自主实例的del以确保一致性。
 *然而,即使从机也会尝试为函数提供一个一致的返回值,这样在从机端执行的read命令将能够像密钥过期一样工作,即使key仍然存在(因为主机尚未传播DEL)
 *
 * In masters as a side effect of finding a key which is expired, such
 * key will be evicted from the database. Also this may trigger the
 * propagation of a DEL/UNLINK command in AOF / replication stream.
 *在masters中,作为查找过期key的副作用,此类key将从数据库中逐出。
 *这也可能触发AOF/复制流中DEL/UNLINK命令的传播
 *
 * The return value of the function is 0 if the key is still valid,
 * otherwise the function returns 1 if the key is expired.
 *如果key仍然有效,则函数返回值为 0,否则如果key已过期,则函数返回 1
 */
int expireIfNeeded(redisDb *db, robj *key) {
    if (!keyIsExpired(db,key)) return 0;

    /* If we are running in the context of a slave, instead of
     * evicting the expired key from the database, we return ASAP:
     * the slave key expiration is controlled by the master that will
     * send us synthesized DEL operations for expired keys.
     * 如果我们在一个 slave 的上下文中运行,而不是从database中驱逐过期的key,我们尽快返回
       slave key 的过期时间由 master 控制,将向我们发送过期key的 DEL 操作

     * 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.
        我们仍然尝试将正确的信息返回给调用者,也就是说如果是0,我们认为key应该仍然有效
        如果是 1,我们认为此时密钥已过期
     */

    if (server.masterhost != NULL) return 1;

    /* If clients are paused, we keep the current dataset constant,
     * but return to the client what we believe is the right state. Typically,
     * at the end of the pause we will properly expire the key OR we will
     * have failed over and the new primary will send us the expire. */
     /*如果客户端暂停,我们将保持当前数据集不变,但将我们认为正确的状态返回给客户端
     *通常,在暂停结束时,我们将正确地使key过期,或者我们将失败,新的主节点向我们发送过期信息
     */
    if (checkClientPauseTimeoutAndReturnIfPaused()) return 1;
    /*删除key,lazyfree_lazy_expire 是Redis的配置项之一,它的作用是是否开启惰性删除 (默认不开启),*/
    /* Delete the key */
    if (server.lazyfree_lazy_expire) {
         //如果是惰性删除,走的是异步删除
        dbAsyncDelete(db,key);
    } else {
      //否则走的是同步删除
        dbSyncDelete(db,key);
    }
    //统计过期 keys的数量
    server.stat_expiredkeys++;
    //向其他salve节点传播过期的key
    propagateExpire(db,key,server.lazyfree_lazy_expire);
    notifyKeyspaceEvent(NOTIFY_EXPIRED,
        "expired",key,db->id);
    //每次修改数据库中的键时,都会调用函数 signalModifiedKey(),如果某个key被监视,下次执行取消监视
    signalModifiedKey(NULL,db,key);
    return 1;
}

这里判断完这个是否过期就去删除这个key了,所以这个方法即是判断key是否过期的方法,里面也加了删除key的方法,虽然这个删除不是彻底删除

在代码上也能看出对于从节点判断过期是有快速失败的,只有主节点才能判断key是否过期,这样master->salve的执行链就可以保证

server.lazyfree_lazy_expire 是redis的配置,来指定查询到key过期时是采用同步的方式还是异步的方式来删除key,下面有同步删除的源码和异步删除的源码

看到这里其实你也就理解了,不管设不设置定时删除策略,在查询时都会有惰性删除策略,这个是写在代码里的,不是配置的,关键就在expireIfNeeded方法

3、惰性删除方法源码

/* Delete a key, value, and associated expiration entry if any, from the DB.
 * If there are enough allocations to free the value object may be put into
 * a lazy free list instead of being freed synchronously. The lazy free list
 * will be reclaimed in a different bio.c thread.
 *如果存在则从数据库中删除key、值和相关过期条目,
 *如果有足够的分配来释放值对象,则可以将其放入延迟释放列表中,而不是同步释放
 *空闲列表将在另一个bio.c线程中回收。
 */
#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
    /* Deleting an entry from the expires dict will not free the sds of
     * the key, because it is shared with the main dictionary.
     * 从过期 dict中删除条目不会释放key的sds,因为它与主词典共享
     */
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

    /* If the value is composed of a few allocations, to free in a lazy way
     * is actually just slower... So under a certain limit we just free
     * the object synchronously.
      *如果值由几个或多个组成,以懒惰的方式释放实际上只是速度较慢。。。因此,在一定的限制下,我们只是同步释放对象
      **/
    dictEntry *de = dictUnlink(db->dict,key->ptr);
    if (de) {
        robj *val = dictGetVal(de);

        /* Tells the module that the key has been unlinked from the database.
        * 告诉模块,这个key已从数据库中取消连接
        */
        moduleNotifyKeyUnlink(key,val);

        size_t free_effort = lazyfreeGetFreeEffort(key,val);

        /* If releasing the object is too much work, do it in the background
         * by adding the object to the lazy free list.
         * Note that if the object is shared, to reclaim it now it is not
         * possible. This rarely happens, however sometimes the implementation
         * of parts of the Redis core may call incrRefCount() to protect
         * objects, and then call dbDelete(). In this case we'll fall
         * through and reach the dictFreeUnlinkedEntry() call, that will be
         * equivalent to just calling decrRefCount().
         *如果释放对象的工作量太大,可以在后台通过将对象添加到空闲列表来完成。注意,如果对象是共享的,那么现在要回收它就不是了
         * 可能的这种情况很少发生,但是有时候Redis核心部分的实现可能会调用incrRefCount()来保护对象,然后调用dbDelete()。
         * 在本例中,我们将完成并到达dictFreeUnlinkdentry()调用,这相当于只调用decrefCount()。
         */
        if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
            atomicIncr(lazyfree_objects,1);
            bioCreateLazyFreeJob(lazyfreeFreeObject,1, val);
            dictSetVal(db->dict,de,NULL);
        }
    }

    /* Release the key-val pair, or just the key if we set the val
     * field to NULL in order to lazy free it later.
     *释放key-val对,或者如果我们将val字段设置为NULL,则只释放key,以便稍后延迟释放它
     */
    if (de) {
        dictFreeUnlinkedEntry(db->dict,de);
        if (server.cluster_enabled) slotToKeyDel(key->ptr);
        return 1;
    } else {
        return 0;
    }
}

4、同步删除方法源码

/* Delete a key, value, and associated expiration entry if any, from the DB
*如果存在则从数据库中删除key、值和相关过期条目,
*/
int dbSyncDelete(redisDb *db, robj *key) {
    /* Deleting an entry from the expires dict will not free the sds of
     * the key, because it is shared with the main dictionary.
     *从过期 dict中删除条目不会释放key的sds,因为它与主词典共享
      */
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
    dictEntry *de = dictUnlink(db->dict,key->ptr);
    if (de) {
        robj *val = dictGetVal(de);
        /* Tells the module that the key has been unlinked from the database.
        * 告诉模块,这个key已从数据库中取消连接
        */
        moduleNotifyKeyUnlink(key,val);
        dictFreeUnlinkedEntry(db->dict,de);
        if (server.cluster_enabled) slotToKeyDel(key->ptr);
        return 1;
    } else {
        return 0;
    }
}

5、用户命令发出删除key时的redis如何选择删除

既然讲到同步异步删除key的方法了,那如果用户主动去删除key会有什么样的执行逻辑呢?

void delCommand(client *c) {
    //lazyfree_lazy_user_del 是否启动懒惰删除
    delGenericCommand(c,server.lazyfree_lazy_user_del);
}

void unlinkCommand(client *c) {
    delGenericCommand(c,1);
}

这两个方法最终调用的都是delGenericCommand(client *c, int lazy),只是unlinkCommand默认lazy是1,走的是异步删除key的方式,而delCommand需要读取配置文件,删除采用的是同步的还是异步(懒惰)的方式需要由配置文件中的参数server.lazyfree_lazy_user_del 决定

/* This command implements DEL and LAZYDEL.
 * 此命令实现DEL和懒惰的DEL
*/
void delGenericCommand(client *c, int lazy) {
    int numdel = 0, j;

    for (j = 1; j < c->argc; j++) {
        expireIfNeeded(c->db,c->argv[j]);
        int deleted  = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
                              dbSyncDelete(c->db,c->argv[j]);
        if (deleted) {
            signalModifiedKey(c,c->db,c->argv[j]);
            notifyKeyspaceEvent(NOTIFY_GENERIC,
                "del",c->argv[j],c->db->id);
            server.dirty++;
            numdel++;
        }
    }
    addReplyLongLong(c,numdel);
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值