以前對非同步刪除幾個引數的作用比較模糊,包括網上的很多資料都是一筆帶過,語焉不詳。
所以這次從原始碼(基於 Redis 7.0.5)的角度來深入分析下這幾個引數的具體作用:
在 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
命令一樣。
在 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 中,FLUSHALL
和FLUSHDB
命令又新增了一個 SYNC 修飾符,它的效果與之前的FLUSHALL
和FLUSHDB
命令一樣,都是用來進行同步刪除操作。
既然效果一樣,為什麼要引入這個修飾符呢?這實際上與 Redis 6.2.0 中引入的 lazyfree-lazy-user-flush 引數有關。該引數控制了沒有加修飾符的FLUSHALL
和FLUSHDB
命令的行為。
預設情況下,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 主要用在兩個函數中:dbDelete
和dbOverwrite
。這兩個函數的實現程式碼如下:
/* 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 函數主要用於 Server 端的一些內部刪除操作,常用於以下場景:
執行MIGRATE
命令時,刪除源端範例的 KEY。
RESTORE
命令中,如果指定了 REPLACE 選項,當指定的 KEY 存在時,會呼叫 dbDelete 刪除這個 KEY。
通過POP
、TRIM
之類的命令從列表(List),集合(Set),有序集合(Sorted Set)中彈出或者移除元素時,當 KEY 為空時,會呼叫 dbDelete 刪除這個 KEY。
SINTERSTORE
、ZINTERSTORE
等 STORE 命令中。這些命令會計算多個集合(有序集合)的交集、並集、差集,並將結果儲存在一個新的 KEY 中。如果交集、並集、差集的結果為空,當用來儲存的 KEY 存在時,會呼叫 dbDelete 刪除這個 KEY。
dbOverwrite 主要是用於 KEY 存在的場景,新值覆蓋舊值。主要用於以下場景:
SET
相關的命令。如 SET
,SETNX
,SETEX
,HSET
,MSET
。SINTERSTORE
、ZINTERSTORE
等 STORE 命令中。如果交集、並集、差集的結果不為空,且用來儲存的 KEY 存在,則該 KEY 的值會通過 dbOverwrite 覆蓋。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
,PEXPIRE
, EXPIREAT
, PEXPIREAT
命令中,當設定的時間過期時(譬如 EXPIRE/PEXPIRE 中指定了負值或者 EXPIREAT/PEXPIREAT指定了過去的時間戳),將導致 KEY 被刪除而不是過期。
4. GETEX
命令中,如果通過 EXAT unix-time-seconds 或者 PXAT unix-time-milliseconds 指定了過期時間,當指定的時間戳過期時,將導致 KEY 被刪除而不是過期。
當 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++;
...
}
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。
另外,在通過POP
、TRIM
之類的命令從列表(List),集合(Set),有序集合(Sorted Set)中彈出或者移除元素時,對於這些元素的刪除都是同步的,並不會非同步刪除。如果元素的值過大(最大值由 proto-max-bulk-len 決定,預設是 512MB),依然會阻塞主執行緒。