关于如何优雅关闭 Redis 服务

方案一:shutdown 命令

Redis 自身提供 shutdown 命令,可关闭 Redis 服务。

SHUTDOWN [NOSAVE | SAVE] [NOW] [FORCE] [ABORT]

shutdown

ACL categories: @admin, @slow, @dangerous

该命令行为如下:

  • 如果存在任何复制延迟的从节点(replicas):
    • 对尝试执行写操作的客户端执行 CLIENT PAUSE WRITE,暂停其写入。
    • 等待最多配置的 shutdown-timeout 时间(默认为 10 秒),让从节点追上主节点的复制偏移量。
  • 停止所有客户端连接。
  • 如果配置了至少一个 SAVE,则执行一次阻塞式的 SAVE 操作(将数据快照写入 RDB 文件)。
  • 如果启用了 AOF(Append Only File),则将 AOF 缓冲区内容刷写(flush)到磁盘。
  • 退出 Redis 服务器。

查看执行 shutdown 命令的日志如下:

70684:M 20 Jan 2026 11:55:51.250 * Ready to accept connections
70684:M 20 Jan 2026 11:56:22.323 # User requested shutdown...   # redis 服务被关闭
70684:M 20 Jan 2026 11:56:22.323 * Saving the final RDB snapshot before exiting.
70684:M 20 Jan 2026 11:56:22.325 * DB saved on disk
70684:M 20 Jan 2026 11:56:22.325 * Removing the pid file.
70684:M 20 Jan 2026 11:56:22.325 # Redis is now ready to exit, bye bye...

我们查看一下 Redis 中 shutdown 命令的源代码:

void shutdownCommand(client *c) {
    int flags = 0;

    if (c->argc > 2) {
        addReply(c,shared.syntaxerr);
        return;
    } else if (c->argc == 2) {
        if (!strcasecmp(c->argv[1]->ptr,"nosave")) {
            flags |= SHUTDOWN_NOSAVE;
        } else if (!strcasecmp(c->argv[1]->ptr,"save")) {
            flags |= SHUTDOWN_SAVE;
        } else {
            addReply(c,shared.syntaxerr);
            return;
        }
    }
    if (prepareForShutdown(flags) == C_OK) exit(0);
    addReplyError(c,"Errors trying to SHUTDOWN. Check logs.");
}

int prepareForShutdown(int flags) {
    /* When SHUTDOWN is called while the server is loading a dataset in
     * memory we need to make sure no attempt is performed to save
     * the dataset on shutdown (otherwise it could overwrite the current DB
     * with half-read data).
     *
     * Also when in Sentinel mode clear the SAVE flag and force NOSAVE. */
    if (server.loading || server.sentinel_mode)
        flags = (flags & ~SHUTDOWN_SAVE) | SHUTDOWN_NOSAVE;

    int save = flags & SHUTDOWN_SAVE;
    int nosave = flags & SHUTDOWN_NOSAVE;

    serverLog(LL_WARNING,"User requested shutdown...");
    if (server.supervised_mode == SUPERVISED_SYSTEMD)
        redisCommunicateSystemd("STOPPING=1\n");

    /* Kill all the Lua debugger forked sessions. */
    ldbKillForkedSessions();

    /* Kill the saving child if there is a background saving in progress.
       We want to avoid race conditions, for instance our saving child may
       overwrite the synchronous saving did by SHUTDOWN. */
    if (server.rdb_child_pid != -1) {
        serverLog(LL_WARNING,"There is a child saving an .rdb. Killing it!");
        /* Note that, in killRDBChild, we call rdbRemoveTempFile that will
         * do close fd(in order to unlink file actully) in background thread.
         * The temp rdb file fd may won't be closed when redis exits quickly,
         * but OS will close this fd when process exits. */
        killRDBChild();
    }

    /* Kill module child if there is one. */
    if (server.module_child_pid != -1) {
        serverLog(LL_WARNING,"There is a module fork child. Killing it!");
        TerminateModuleForkChild(server.module_child_pid,0);
    }

    if (server.aof_state != AOF_OFF) {
        /* Kill the AOF saving child as the AOF we already have may be longer
         * but contains the full dataset anyway. */
        if (server.aof_child_pid != -1) {
            /* If we have AOF enabled but haven't written the AOF yet, don't
             * shutdown or else the dataset will be lost. */
            if (server.aof_state == AOF_WAIT_REWRITE) {
                serverLog(LL_WARNING, "Writing initial AOF, can't exit.");
                return C_ERR;
            }
            serverLog(LL_WARNING,
                "There is a child rewriting the AOF. Killing it!");
            killAppendOnlyChild();
        }
        /* Append only file: flush buffers and fsync() the AOF at exit */
        serverLog(LL_NOTICE,"Calling fsync() on the AOF file.");
        flushAppendOnlyFile(1);
        redis_fsync(server.aof_fd);
    }

    /* Create a new RDB file before exiting. */
    if ((server.saveparamslen > 0 && !nosave) || save) {
        serverLog(LL_NOTICE,"Saving the final RDB snapshot before exiting.");
        if (server.supervised_mode == SUPERVISED_SYSTEMD)
            redisCommunicateSystemd("STATUS=Saving the final RDB snapshot\n");
        /* Snapshotting. Perform a SYNC SAVE and exit */
        rdbSaveInfo rsi, *rsiptr;
        rsiptr = rdbPopulateSaveInfo(&rsi);
        if (rdbSave(server.rdb_filename,rsiptr) != C_OK) {
            /* Ooops.. error saving! The best we can do is to continue
             * operating. Note that if there was a background saving process,
             * in the next cron() Redis will be notified that the background
             * saving aborted, handling special stuff like slaves pending for
             * synchronization... */
            serverLog(LL_WARNING,"Error trying to save the DB, can't exit.");
            if (server.supervised_mode == SUPERVISED_SYSTEMD)
                redisCommunicateSystemd("STATUS=Error trying to save the DB, can't exit.\n");
            return C_ERR;
        }
    }

    /* Fire the shutdown modules event. */
    moduleFireServerEvent(REDISMODULE_EVENT_SHUTDOWN,0,NULL);

    /* Remove the pid file if possible and needed. */
    if (server.daemonize || server.pidfile) {
        serverLog(LL_NOTICE,"Removing the pid file.");
        unlink(server.pidfile);
    }

    /* Best effort flush of slave output buffers, so that we hopefully
     * send them pending writes. */
    flushSlavesOutputBuffers();

    /* Close the listening sockets. Apparently this allows faster restarts. */
    closeListeningSockets(1);
    serverLog(LL_WARNING,"%s is now ready to exit, bye bye...",
        server.sentinel_mode ? "Sentinel" : "Redis");
    return C_OK;
}

方案二:systemd 关闭 Redis 服务

可以使用 systemd 管理 Redis 服务。

  1. 创建 redis.service 文件
[Unit]
Description=Redis In-Memory Database
Documentation=https://redis.io/docs/
After=network.target
After=network-online.target
Wants=network-online.target

[Service]
# notify 允许 Redis 主动通知 systemd(通过 sd_notify)其启动已完成。
Type=notify
ExecStart=/usr/local/redis/bin/redis-server /etc/redis/redis.conf --supervised systemd
# 失败时重启
Restart=on-failure
RestartSec=5
# 停止超时设置为 60 秒,允许足够时间进行持久化
TimeoutStopSec=60
# 启动超时设置为 10 分钟,防止在大量数据加载时超时
TimeoutStartSec=600
User=redis
Group=redis
LimitNOFILE=65536
PrivateTmp=yes
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_SYS_RESOURCE
# 降低被 OOM Killer 杀死的概率
OOMScoreAdjust=-1000
# 必须是相对路径(相对于 /run),不能写绝对路径
RuntimeDirectory=redis

[Install]
WantedBy=multi-user.target
  1. 通过 systemctl start redis 启动 Redis 服务。

Redis 服务启动时,会向 systemd 发送 STATUS=Ready to accept connections 信号,并设置 READY=1

        if (server.supervised_mode == SUPERVISED_SYSTEMD) {
            redisCommunicateSystemd("STATUS=Ready to accept connections\n");
            redisCommunicateSystemd("READY=1\n");
        }

Redis 启动时,会注册信号处理函数:

void initServer(void) {
    int j;

    signal(SIGHUP, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    setupSignalHandlers();    // 注册信号处理函数
    makeThreadKillable();
    // ...
}

void setupSignalHandlers(void) {
    struct sigaction act;

    /* When the SA_SIGINFO flag is set in sa_flags then sa_sigaction is used.
     * Otherwise, sa_handler is used. */
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    act.sa_handler = sigShutdownHandler;
    sigaction(SIGTERM, &act, NULL);
    sigaction(SIGINT, &act, NULL);
    // ...
}

static void sigShutdownHandler(int sig) {
    char *msg;

    switch (sig) {
    case SIGINT:
        msg = "Received SIGINT scheduling shutdown...";
        break;
    case SIGTERM:
        msg = "Received SIGTERM scheduling shutdown...";
        break;
    default:
        msg = "Received shutdown signal, scheduling shutdown...";
    };

    /* SIGINT is often delivered via Ctrl+C in an interactive session.
     * If we receive the signal the second time, we interpret this as
     * the user really wanting to quit ASAP without waiting to persist
     * on disk. */
    if (server.shutdown_asap && sig == SIGINT) {
        serverLogFromHandler(LL_WARNING, "You insist... exiting now.");
        rdbRemoveTempFile(getpid(), 1);
        exit(1); /* Exit with an error since this was not a clean shutdown. */
    } else if (server.loading) {
        serverLogFromHandler(LL_WARNING, "Received shutdown signal during loading, exiting now.");
        exit(0);
    }

    serverLogFromHandler(LL_WARNING, msg);
    server.shutdown_asap = 1;    // SHUTDOWN needed ASAP
}

收到 TERM 信号后,会将 server.shutdown_asap = 1,在 serverCron 中会调用 prepareForShutdown 函数关闭服务,其执行内容与 shutdown 命令相同。serverCron 并非由外部直接调用,而是通过 Redis 内部的事件驱动框架(ae)注册为一个时间事件,由主事件循环周期性地触发。

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    // ...

    /* We received a SIGTERM, shutting down here in a safe way, as it is
     * not ok doing so inside the signal handler. */
    if (server.shutdown_asap) {
        if (prepareForShutdown(SHUTDOWN_NOFLAGS) == C_OK) exit(0);
        serverLog(LL_WARNING,"SIGTERM received but errors trying to shut down the server, check the logs for more information");
        server.shutdown_asap = 0;
    }

    // ...
}

查看日志:

58078:C 20 Jan 2026 11:10:27.966 * Supervised by systemd. Please make sure you set appropriate values for TimeoutStartSec and TimeoutStopSec in your service unit.
58078:C 20 Jan 2026 11:10:27.966 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
58078:C 20 Jan 2026 11:10:27.966 # Redis version=7.0.15, bits=64, commit=00000000, modified=0, pid=58078, just started
58078:C 20 Jan 2026 11:10:27.966 # Configuration loaded
58078:M 20 Jan 2026 11:10:27.967 * monotonic clock: POSIX clock_gettime
58078:M 20 Jan 2026 11:10:27.969 * Running mode=standalone, port=6379.
58078:M 20 Jan 2026 11:10:27.969 # Server initialized
58078:M 20 Jan 2026 11:10:27.972 * Loading RDB produced by version 7.0.15
58078:M 20 Jan 2026 11:10:27.972 * RDB age 43530 seconds
58078:M 20 Jan 2026 11:10:27.972 * RDB memory usage when created 0.90 Mb
58078:M 20 Jan 2026 11:10:27.972 * Done loading RDB, keys loaded: 0, keys expired: 0.
58078:M 20 Jan 2026 11:10:27.972 * DB loaded from disk: 0.001 seconds
58078:M 20 Jan 2026 11:10:27.972 * Ready to accept connections

可以看到日志中输出:Supervised by systemd. Please make sure you set appropriate values for TimeoutStartSec and TimeoutStopSec in your service unit.,说明 Redis 服务被 systemd 管理。

  1. 通过 systemctl stop redis 停止 Redis 服务。Redis 收到 SIGTERM 信号后,进行信号处理。
58078:signal-handler (1768878767) Received SIGTERM scheduling shutdown...
58078:M 20 Jan 2026 11:12:47.393 # User requested shutdown...
58078:M 20 Jan 2026 11:12:47.393 * Saving the final RDB snapshot before exiting.
58078:M 20 Jan 2026 11:12:47.395 * DB saved on disk
58078:M 20 Jan 2026 11:12:47.395 * Removing the pid file.
58078:M 20 Jan 2026 11:12:47.395 # Redis is now ready to exit, bye bye...

如果 redis.service 文件配置了 ExecStop,则使用此命令关闭 Redis 服务。如果没有配置 ExecStop,则当执行 systemctl stop redis 时,systemd 默认会向主进程($MAINPID)发送 SIGTERM 信号,然后等待主进程处理完信号 SIGTERM 后退出。

# 发送 SIGTERM 允许 Redis 进行优雅关闭(持久化等)
ExecStop=/bin/kill -s TERM $MAINPID

补充知识点

当一个 systemd 服务配置为 Type=notify 时,systemd 会等待服务进程显式调用 sd_notify() 发送 "READY=1" 信号,才认为服务已成功启动并进入运行状态。

典型流程:

  1. systemd 启动服务进程
  2. 服务完成初始化(如加载配置、绑定端口)
  3. 调用 sd_notify(0, "READY=1")
  4. systemd 将服务状态从 activating 切换为 active (running)

"READY=1" 表示服务已启动就绪(最关键!)

工作原理:

  1. systemd 在启动服务时,会设置一个 Unix domain socket 路径到环境变量 NOTIFY_SOCKET(如 /run/systemd/notify)。
  2. sd_notify() 通过这个 socket 向 systemd 发送消息。
  3. systemd 解析消息并更新服务状态。

Redis 哨兵模式

Redis 哨兵的关机执行命令与 Redis 主进程的关机命令相同。

struct redisCommand sentinelcmds[] = {
    {"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
    {"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
    {"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
    {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
    {"role",sentinelRoleCommand,1,"ok-loading",0,NULL,0,0,0,0,0},
    {"client",clientCommand,-2,"read-only no-script",0,NULL,0,0,0,0,0},
    {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0},
    {"auth",authCommand,2,"no-auth no-script ok-loading ok-stale fast",0,NULL,0,0,0,0,0},
    {"hello",helloCommand,-2,"no-auth no-script fast",0,NULL,0,0,0,0,0}
};

哨兵模式下,启动时注册信号处理函数,与 Redis 相同。

Redis 6.0 与 Redis 7.0 的区别

Redis 7.0 相比 Redis 6.0 增加了 shutdown-timeout 参数以及 shutdown-on-sigintshutdown-on-sigterm 参数。

################################ SHUTDOWN #####################################

# Maximum time to wait for replicas when shutting down, in seconds.
#
# During shut down, a grace period allows any lagging replicas to catch up with
# the latest replication offset before the master exists. This period can
# prevent data loss, especially for deployments without configured disk backups.
#
# The 'shutdown-timeout' value is the grace period's duration in seconds. It is
# only applicable when the instance has replicas. To disable the feature, set
# the value to 0.
#
# shutdown-timeout 10

# When Redis receives a SIGINT or SIGTERM, shutdown is initiated and by default
# an RDB snapshot is written to disk in a blocking operation if save points are configured.
# The options used on signaled shutdown can include the following values:
# default:  Saves RDB snapshot only if save points are configured.
#           Waits for lagging replicas to catch up.
# save:     Forces a DB saving operation even if no save points are configured.
# nosave:   Prevents DB saving operation even if one or more save points are configured.
# now:      Skips waiting for lagging replicas.
# force:    Ignores any errors that would normally prevent the server from exiting.
#
# Any combination of values is allowed as long as "save" and "nosave" are not set simultaneously.
# Example: "nosave force now"
#
# shutdown-on-sigint default
# shutdown-on-sigterm default

经验值

Redis 正常关机需要多久?这里测试 10G 数据量,关机耗时 3 分钟左右,具体时间根据测试物理机性能的不同而不同,仅供参考。

39691:signal-handler (1769503068) Received SIGTERM scheduling shutdown...
39691:S 27 Jan 2026 16:37:48.264 # User requested shutdown...
39691:S 27 Jan 2026 16:37:48.264 * Saving the final RDB snapshot before exiting.
39691:S 27 Jan 2026 16:40:40.362 * DB saved on disk
39691:S 27 Jan 2026 16:40:40.362 * Removing the pid file.
39691:S 27 Jan 2026 16:40:40.362 # Redis is now ready to exit, bye bye...

Redis 开机耗时?这里测试加载 10G 左右的 RDB 的情况:主节点加载 RDB 耗时 18s 左右。

 535 125059:M 27 Jan 2026 16:55:50.576 # Server initialized
 536 125059:M 27 Jan 2026 16:55:50.577 * Loading RDB produced by version 6.0.20
 537 125059:M 27 Jan 2026 16:55:50.577 * RDB age 909 seconds
 538 125059:M 27 Jan 2026 16:55:50.577 * RDB memory usage when created 12568.23 Mb
 539 125059:M 27 Jan 2026 16:56:08.615 * DB loaded from disk: 18.038 seconds

从节点主从同步大概耗时 3 分钟左右。

44911:S 27 Jan 2026 16:56:09.958 # Server initialized
44911:S 27 Jan 2026 16:56:09.959 * Loading RDB produced by version 6.0.20
44911:S 27 Jan 2026 16:56:09.959 * RDB age 1101 seconds
44911:S 27 Jan 2026 16:56:09.959 * RDB memory usage when created 12569.46 Mb
44911:S 27 Jan 2026 16:56:27.996 * DB loaded from disk: 18.037 seconds
44911:S 27 Jan 2026 16:56:27.996 * Before turning into a replica, using my own master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
44911:S 27 Jan 2026 16:56:27.996 * Ready to accept connections
44911:S 27 Jan 2026 16:56:27.996 - DB 4: 1000 keys (0 volatile) in 1024 slots HT.
44911:S 27 Jan 2026 16:56:27.996 . 0 clients connected (0 replicas), 13111588928 bytes in use
44911:S 27 Jan 2026 16:56:27.996 * Connecting to MASTER 101.66.174.21:7788
44911:S 27 Jan 2026 16:56:27.997 * MASTER <-> REPLICA sync started
44911:S 27 Jan 2026 16:56:27.997 * Non blocking connect for SYNC fired the event.
44911:S 27 Jan 2026 16:56:27.997 * Master replied to PING, replication can continue...
44911:S 27 Jan 2026 16:56:27.998 * Trying a partial resynchronization (request ece1ad4c54ae60bf15c56bad0830215be54d8bca:10486559107).
44911:S 27 Jan 2026 16:56:28.107 * Full resync from master: b055ca953ecd85a9ad8eb80b381d381b42458365:0
44911:S 27 Jan 2026 16:56:28.107 * Discarding previously cached master state.
44911:S 27 Jan 2026 16:56:33.009 - DB 4: 1000 keys (0 volatile) in 1024 slots HT.
//  ...

44911:S 27 Jan 2026 16:59:18.494 . 0 clients connected (0 replicas), 13111568192 bytes in use
44911:S 27 Jan 2026 16:59:20.310 * MASTER <-> REPLICA sync: receiving 10485916109 bytes from master to disk
44911:S 27 Jan 2026 16:59:23.499 - DB 4: 1000 keys (0 volatile) in 1024 slots HT.
44911:S 27 Jan 2026 16:59:23.499 . 0 clients connected (0 replicas), 13111568192 bytes in use
44911:S 27 Jan 2026 16:59:28.499 - DB 4: 1000 keys (0 volatile) in 1024 slots HT.
44911:S 27 Jan 2026 16:59:28.499 . 0 clients connected (0 replicas), 13111568192 bytes in use
44911:S 27 Jan 2026 16:59:33.499 - DB 4: 1000 keys (0 volatile) in 1024 slots HT.
44911:S 27 Jan 2026 16:59:33.499 . 0 clients connected (0 replicas), 13111568192 bytes in use
44911:S 27 Jan 2026 16:59:38.515 - DB 4: 1000 keys (0 volatile) in 1024 slots HT.
44911:S 27 Jan 2026 16:59:38.515 . 0 clients connected (0 replicas), 13111568192 bytes in use
44911:S 27 Jan 2026 16:59:38.888 * MASTER <-> REPLICA sync: Flushing old data
44911:S 27 Jan 2026 16:59:38.900 * MASTER <-> REPLICA sync: Loading DB in memory
44911:S 27 Jan 2026 16:59:38.954 * Loading RDB produced by version 6.0.20
44911:S 27 Jan 2026 16:59:38.954 * RDB age 190 seconds
44911:S 27 Jan 2026 16:59:38.954 * RDB memory usage when created 12568.22 Mb
44911:S 27 Jan 2026 16:59:56.914 * MASTER <-> REPLICA sync: Finished with success