以前对异步删除几个参数的作用比较模糊,包括网上的很多资料都是一笔带过,语焉不详。
所以这次从源码(基于 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。
[code]if (iAmMaster() &&
!(rdbflags & RDBFLAGS_AOF_PREAMBLE) &&
expiretime != -1 && expiretime |