以前对异步删除几个参数的作用比较模糊,包括网上的很多资料都是一笔带过,语焉不详。

所以这次从源码(基于 Redis 7.0.5)的角度来深入分析下这几个参数的具体作用:

  • lazyfree-lazy-user-del
  • lazyfree-lazy-user-flush
  • lazyfree-lazy-server-del
  • lazyfree-lazy-expire
  • lazyfree-lazy-eviction
  • slave-lazy-flush

lazyfree-lazy-user-del

在 Redis 4.0 之前,通常不建议直接使用 DEL 命令删除一个 KEY。这是因为,如果这个 KEY 是一个包含大量数据的大 KEY,那么这个删除操作就会阻塞主线程,导致 Redis 无法处理其他请求。这种情况下,一般是建议分而治之,即批量删除 KEY 中的元素。

在 Redis 4.0 中,引入了异步删除机制,包括一个新的命令 -UNLINK。该命令的作用同DEL一样,都用来删除 KEY。只不过DEL命令是在主线程中同步执行删除操作。而UNLINK命令则是通过后台线程异步执行删除操作,即使碰到一个大 KEY,也不会导致主线程被阻塞。

如果应用之前用的是DEL,要使用UNLINK,就意味着代码需要改造,而代码改造显然是个费时费力的事情。

为了解决这个痛点,在 Redis 6.0 中,引入了参数 lazyfree-lazy-user-del。将该参数设置为 yes(默认为 no),则通过DEL命令删除 KEY,效果同UNLINK一样,都是执行异步删除操作。

以下是DEL命令和UNLINK命令的实现代码。

// DEL 命令调用的函数
void delCommand(client *c) {
    delGenericCommand(c,server.lazyfree_lazy_user_del);
}

// UNLINK 命令调用的函数
void unlinkCommand(client *c) {
    delGenericCommand(c,1);
}

可以看到,当 server.lazyfree_lazy_user_del 设置为 yes 时,DEL命令实际上调用的就是 delGenericCommand(c,1),与UNLINK命令一样。

lazyfree-lazy-user-flush

在 Redis 中,如果要清除整个数据库的数据,可使用FLUSHALL(清除所有数据库的数据)或 FLUSHDB(清除当前数据库的数据)。

在 Redis 4.0 之前,这两个命令都是在主线程中执行的。如果要清除的 KEY 比较多,同样会导致主线程被阻塞。

如果使用的是 Redis Cluster,在执行此类操作时,很容易会触发主从切换。

主要原因是在删除期间,主节点无法响应集群其它节点的心跳请求。如果没有响应持续的时间超过 cluster-node-timeout(默认15 秒),主节点就会被集群其它节点判定为故障,进而触发故障切换流程,将从节点提升为主节点。

这个时候,原主节点会降级为从节点,降级后,原主节点又会重新从新的主节点上同步数据。所以,虽然原主节点上执行了 FLUSH 操作,但发生故障切换后,数据又同步过来了。如果再对新的主节点执行 FLUSH 操作,同样会触发主从切换。

所以,在这种情况下,建议将参数 cluster-node-timeout 调整为一个比较大的值(默认是 15 秒),这样就可以确保主节点有充足的时间来执行 FLUSH 操作而不会触发切换流程。

在 Redis 4.0 中,FLUSHALL 和 FLUSHDB 命令新增了一个 ASYNC 修饰符,可用来进行异步删除操作。如果不加 ASYNC,则还是主线程同步删除。

FLUSHALL ASYNC
FLUSHDB ASYNC

在 Redis 6.2.0 中,FLUSHALLFLUSHDB命令又新增了一个 SYNC 修饰符,它的效果与之前的FLUSHALLFLUSHDB命令一样,都是用来进行同步删除操作。

既然效果一样,为什么要引入这个修饰符呢?这实际上与 Redis 6.2.0 中引入的 lazyfree-lazy-user-flush 参数有关。该参数控制了没有加修饰符的FLUSHALLFLUSHDB命令的行为。

默认情况下,lazyfree-lazy-user-flush 的值为 no,这意味着 FLUSHALL/FLUSHDB 将执行同步删除操作。如果将 lazyfree-lazy-user-flush 设置为 yes,即使不加 ASYNC 修饰符,FLUSHALL/FLUSHDB 也会进行异步删除。

以下是 lazyfree-lazy-user-flush 参数的相关代码:

/* Return the set of flags to use for the emptyDb() call for FLUSHALL
 * and FLUSHDB commands.
 *
 * sync: flushes the database in an sync manner.
 * async: flushes the database in an async manner.
 * no option: determine sync or async according to the value of lazyfree-lazy-user-flush.
 *
 * On success C_OK is returned and the flags are stored in *flags, otherwise
 * C_ERR is returned and the function sends an error to the client. */
int getFlushCommandFlags(client *c, int *flags) {
    /* Parse the optional ASYNC option. */
    if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"sync")) {
        *flags = EMPTYDB_NO_FLAGS;
    } else if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"async")) {
        *flags = EMPTYDB_ASYNC;
    } else if (c->argc == 1) {
        *flags = server.lazyfree_lazy_user_flush ? EMPTYDB_ASYNC : EMPTYDB_NO_FLAGS;
    } else {
        addReplyErrorObject(c,shared.syntaxerr);
        return C_ERR;
    }
    return C_OK;
}

可以看到,在不指定任何修饰符的情况下(c->argc == 1),修饰符的取值由 server.lazyfree_lazy_user_flush 决定。

lazyfree-lazy-server-del

lazyfree-lazy-server-del 主要用在两个函数中:dbDeletedbOverwrite。这两个函数的实现代码如下:

/* This is a wrapper whose behavior depends on the Redis lazy free
 * configuration. Deletes the key synchronously or asynchronously. */
int dbDelete(redisDb *db, robj *key) {
    return dbGenericDelete(db, key, server.lazyfree_lazy_server_del);
}

/* Overwrite an existing key with a new value. Incrementing the reference
 * count of the new value is up to the caller.
 * This function does not modify the expire time of the existing key.
 *
 * The program is aborted if the key was not already present. */
void dbOverwrite(redisDb *db, robj *key, robj *val) {
    dictEntry *de = dictFind(db->dict,key->ptr);
    ...
    if (server.lazyfree_lazy_server_del) {
        freeObjAsync(key,old,db->id);
        dictSetVal(db->dict, &auxentry, NULL);
    }

    dictFreeVal(db->dict, &auxentry);
}

下面我们分别看看这两个函数的使用场景。

dbDelete

dbDelete 函数主要用于 Server 端的一些内部删除操作,常用于以下场景:

  1. 执行MIGRATE命令时,删除源端实例的 KEY。

  2. RESTORE命令中,如果指定了 REPLACE 选项,当指定的 KEY 存在时,会调用 dbDelete 删除这个 KEY。

  3. 通过POPTRIM之类的命令从列表(List),集合(Set),有序集合(Sorted Set)中弹出或者移除元素时,当 KEY 为空时,会调用 dbDelete 删除这个 KEY。

  4. SINTERSTOREZINTERSTORE等 STORE 命令中。这些命令会计算多个集合(有序集合)的交集、并集、差集,并将结果存储在一个新的 KEY 中。如果交集、并集、差集的结果为空,当用来存储的 KEY 存在时,会调用 dbDelete 删除这个 KEY。

dbOverwrite

dbOverwrite 主要是用于 KEY 存在的场景,新值覆盖旧值。主要用于以下场景:

  1. SET 相关的命令。如 SETSETNXSETEXHSETMSET
  2. SINTERSTOREZINTERSTORE 等 STORE 命令中。如果交集、并集、差集的结果不为空,且用来存储的 KEY 存在,则该 KEY 的值会通过 dbOverwrite 覆盖。

lazyfree-lazy-expire

lazyfree-lazy-expire 主要用于以下四种场景:

1. 删除过期 KEY,包括主动访问时删除和 Redis 定期删除。

不仅如此,该参数还决定了删除操作传播给从库及写到 AOF 文件中是用DEL还是UNLINK

/* Delete the specified expired key and propagate expire. */
void deleteExpiredKeyAndPropagate(redisDb *db, robj *keyobj) {
    mstime_t expire_latency;
    latencyStartMonitor(expire_latency);
    // 删除过期 KEY
    if (server.lazyfree_lazy_expire)
        dbAsyncDelete(db,keyobj);
    else
        dbSyncDelete(db,keyobj);
    latencyEndMonitor(expire_latency);
    latencyAddSampleIfNeeded("expire-del",expire_latency);
    notifyKeyspaceEvent(NOTIFY_EXPIRED,"expired",keyobj,db->id);
    signalModifiedKey(NULL, db, keyobj);
    // 将删除操作传播给从库及写到 AOF 文件中
    propagateDeletion(db,keyobj,server.lazyfree_lazy_expire);
    server.stat_expiredkeys++;
}

2. 主库启动,加载 RDB 的时候,当碰到过期 KEY 时,该参数决定了删除操作传播给从库是用DEL还是UNLINK

if (iAmMaster() &&
    !(rdbflags & RDBFLAGS_AOF_PREAMBLE) &&
    expiretime != -1 && expiretime < now)
{
    if (rdbflags & RDBFLAGS_FEED_REPL) {
        /* Caller should have created replication backlog,
         * and now this path only works when rebooting,
         * so we don't have replicas yet. */
        serverAssert(server.repl_backlog != NULL && listLength(server.slaves) == 0);
        robj keyobj;
        initStaticStringObject(keyobj, key);
        robj *argv[2];
        argv[0] = server.lazyfree_lazy_expire ? shared.unlink : shared.del;
        argv[1] = &keyobj;
        replicationFeedSlaves(server.slaves, dbid, argv, 2);
    }
    sdsfree(key);
    decrRefCount(val);
    server.rdb_last_load_keys_expired++;
}

3. EXPIRE,PEXPIREEXPIREATPEXPIREAT命令中,当设置的时间过期时(譬如 EXPIRE/PEXPIRE 中指定了负值或者 EXPIREAT/PEXPIREAT指定了过去的时间戳),将导致 KEY 被删除而不是过期。

4. GETEX命令中,如果通过 EXAT unix-time-seconds 或者 PXAT unix-time-milliseconds 指定了过期时间,当指定的时间戳过期时,将导致 KEY 被删除而不是过期。

lazyfree-lazy-eviction

当 Redis 内存不足时,会删除部分 KEY 来释放内存。

lazyfree-lazy-eviction 决定了KEY 删除的方式及删除操作传播给从库和写到 AOF 文件中是用DEL还是UNLINK

/* Finally remove the selected key. */
if (bestkey) {
    db = server.db + bestdbid;
    robj *keyobj = createStringObject(bestkey, sdslen(bestkey));
    delta = (long long) zmalloc_used_memory();
    latencyStartMonitor(eviction_latency);
    // 删除 KEY 
    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);
    // 将删除操作传播给从库并写到 AOF 文件中
    propagateDeletion(db, keyobj, server.lazyfree_lazy_eviction);
    decrRefCount(keyobj);
    keys_freed++;
    ...
}

slave-lazy-flush

Redis 主从复制中,从节点在加载主节点的 RDB 文件之前,首先会清除自身的数据,slave-lazy-flush 决定了数据清除的方式。

/* Asynchronously read the SYNC payload we receive from a master */
#define REPL_MAX_WRITTEN_BEFORE_FSYNC (1024*1024*8) /* 8 MB */
void readSyncBulkPayload(connection *conn) {
    char buf[PROTO_IOBUF_LEN];
    ssize_t nread, readlen, nwritten;
    int use_diskless_load = useDisklessLoad();
    redisDb *diskless_load_tempDb = NULL;
    functionsLibCtx* temp_functions_lib_ctx = NULL;
    int empty_db_flags = server.repl_slave_lazy_flush ? EMPTYDB_ASYNC :
                                                        EMPTYDB_NO_FLAGS;
    ...
    if (use_diskless_load && server.repl_diskless_load == REPL_DISKLESS_LOAD_SWAPDB) {
        /* Initialize empty tempDb dictionaries. */
        diskless_load_tempDb = disklessLoadInitTempDb();
        temp_functions_lib_ctx = functionsLibCtxCreate();

        moduleFireServerEvent(REDISMODULE_EVENT_REPL_ASYNC_LOAD,
                              REDISMODULE_SUBEVENT_REPL_ASYNC_LOAD_STARTED,
                              NULL);
    } else {
        replicationAttachToNewMaster();

        serverLog(LL_NOTICE, "MASTER <-> REPLICA sync: Flushing old data");
        emptyData(-1,empty_db_flags,replicationEmptyDbCallback);
    }
    ...
}

总结

综合上面的分析,异步删除各参数的作用如下,

注意,这几个参数的默认值都是 no。

另外,在通过POPTRIM之类的命令从列表(List),集合(Set),有序集合(Sorted Set)中弹出或者移除元素时,对于这些元素的删除都是同步的,并不会异步删除。如果元素的值过大(最大值由 proto-max-bulk-len 决定,默认是 512MB),依然会阻塞主线程。

从源码分析 Redis 异步删除各个参数的具体作用的更多相关文章

  1. jQuery源码分析(九) 异步队列模块 Deferred 详解

    deferred对象就是jQuery的回调函数解决方案,它解决了如何处理耗时操作的问题,比如一些Ajax操作,动画操作等.(P.s:紧跟上一节:https://www.cnblogs.com/grea ...

  2. [Abp vNext 源码分析] - 11. 用户的自定义参数与配置

    一.简要说明 文章信息: 基于的 ABP vNext 版本:1.0.0 创作日期:2019 年 10 月 23 日晚 更新日期:暂无 ABP vNext 针对用户可编辑的配置,提供了单独的 Volo. ...

  3. tornado框架源码分析---Application类之debug参数

    先贴上Application这个类的源码. class Application(httputil.HTTPServerConnectionDelegate): """A ...

  4. dubbo源码分析5——SPI机制_AdaptiveExtension的原理和作用

    private T createAdaptiveExtension() { try { return injectExtension((T) getAdaptiveExtensionClass().n ...

  5. redis源码分析之发布订阅(pub/sub)

    redis算是缓存界的老大哥了,最近做的事情对redis依赖较多,使用了里面的发布订阅功能,事务功能以及SortedSet等数据结构,后面准备好好学习总结一下redis的一些知识点. 原文地址:htt ...

  6. whatwg-fetch源码分析

    fetch 是什么 XMLHttpRequest的最新替代技术 fetch优点 接口更简单.简洁,更加语义化 基于promise,更加好的流程化控制,可以不断then把参数传递,外加 async/aw ...

  7. 【转载】Redis 4.0 自动内存碎片整理(Active Defrag)源码分析

    click原文链接原文链接:https://blog.csdn.net/zouhuajianclever/article/details/90669409阅读本文前建议先阅读此篇博客: Redis源码 ...

  8. 【源码】Redis exists命令bug分析

    本文基于社区版Redis 4.0.8 1.复现条件 版本:社区版Redis 4.0.10以下版本 使用场景:开启读写分离的主从架构或者集群架构(master只负责写流量,slave负责读流量) 案例: ...

  9. Redis学习——ae事件处理源码分析

    0. 前言 Redis在封装事件的处理采用了Reactor模式,添加了定时事件的处理.Redis处理事件是单进程单线程的,而经典Reator模式对事件是串行处理的.即如果有一个事件阻塞过久的话会导致整 ...

  10. redis源码分析之事务Transaction(上)

    这周学习了一下redis事务功能的实现原理,本来是想用一篇文章进行总结的,写完以后发现这块内容比较多,而且多个命令之间又互相依赖,放在一篇文章里一方面篇幅会比较大,另一方面文章组织结构会比较乱,不容易 ...

随机推荐

  1. CGLIB动态代理对象GC问题排查

    一.问题是怎么发现的 最近有个新系统开发完成后要上线,由于系统调用量很大,所以先对核心接口进行了一次压力测试,由于核心接口中基本上只有纯内存运算,所以预估核心接口的压测QPS能够达到上千. 压测容器配 ...

  2. GitOps 与 DevOps:了解关键差异,为企业做出最佳选择

    在软件开发领域,GitOps 和 DevOps 是加强协作和实现软件交付流程自动化的重要技术.虽然这两种模式都旨在提高软件开发生命周期的效率,但它们的核心原则和实施方式却各不相同. 本篇文章将帮助您了 ...

  3. Redis系列18:过期数据的删除策略

    Redis系列1:深刻理解高性能Redis的本质 Redis系列2:数据持久化提高可用性 Redis系列3:高可用之主从架构 Redis系列4:高可用之Sentinel(哨兵模式) Redis系列5: ...

  4. 微信小程序预览时显示有图片未上传

    最近在做小程序项目,在项目里面加了几个图片.在预览调试时出现弹窗显示"文件未上传",但是在左侧的模拟器上却是能正常显示的. 解决思路: 图片在本地和模拟器上显示正常,表示图片本身没 ...

  5. SpringBoot 启动流程追踪(第二篇)

    上一篇文章分析了除 refresh 方法外的流程,并着重分析了 load 方法,这篇文章就主要分析 refresh 方法,可以说 refresh 方法是 springboot 启动流程最重要的一环,没 ...

  6. 【pandas小技巧】--DataFrame的显示样式

    上一篇介绍了DataFrame的显示参数,主要是对DataFrame中值进行调整. 本篇介绍DataFrame的显示样式的调整,显示样式主要是对表格本身的调整,比如颜色,通过颜色可以突出显示重要的值, ...

  7. SpringBoot 笔记

    SpringBoot 笔记 一.Spring Boot 入门 1.Spring Boot 简介 2.微服务 2014,martin fowler 微服务:架构风格(服务微化) 一个应用应该是一组小型服 ...

  8. influxdb 函数 non_negative_derivative 使用

    转载请注明出处: 在InfluxDB中,non_negative_derivative()函数用于计算指定字段的非负导数.它可以用来计算时间序列数据的速率或增长率. 该函数的语法如下: non_neg ...

  9. 使用Skonsole自动生成Git提交信息

    使用Skonsole自动生成Git提交信息 随着LLM应用的普及,日常工作中的很多使用都可以使用LLM来完成,比如Git提交信息的生成. Skonsole是一个基于Semantic Kernel的命令 ...

  10. 要调用API接口获取商品数据,首先需要了解该API的文档和规范

    ​ 要调用API接口获取商品数据,首先需要了解该API的文档和规范.大多数API都需要使用API密钥进行身份验证,因此您需要先注册API提供商,并从他们那里获取API密钥.以下是一些通用的步骤: 1. ...