以前对异步删除几个参数的作用比较模糊,包括网上的很多资料都是一笔带过,语焉不详。
所以这次从源码(基于 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 中,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
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
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
dbOverwrite 主要是用于 KEY 存在的场景,新值覆盖旧值。主要用于以下场景:
SET
相关的命令。如SET
,SETNX
,SETEX
,HSET
,MSET
。SINTERSTORE
、ZINTERSTORE
等 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
,PEXPIRE
, EXPIREAT
, PEXPIREAT
命令中,当设置的时间过期时(譬如 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。
另外,在通过POP
、TRIM
之类的命令从列表(List),集合(Set),有序集合(Sorted Set)中弹出或者移除元素时,对于这些元素的删除都是同步的,并不会异步删除。如果元素的值过大(最大值由 proto-max-bulk-len 决定,默认是 512MB),依然会阻塞主线程。