從原始碼分析 Redis 非同步刪除各個引數的具體作用

2023-11-27 12:02:15

以前對非同步刪除幾個引數的作用比較模糊,包括網上的很多資料都是一筆帶過,語焉不詳。

所以這次從原始碼(基於 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),依然會阻塞主執行緒。