关于如何优雅关闭 Redis 服务
方案一:shutdown 命令
Redis 自身提供 shutdown 命令,可关闭 Redis 服务。
SHUTDOWN [NOSAVE | SAVE] [NOW] [FORCE] [ABORT]

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 服务。
- 创建
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
- 通过
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 管理。
- 通过
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" 信号,才认为服务已成功启动并进入运行状态。
典型流程:
- systemd 启动服务进程
- 服务完成初始化(如加载配置、绑定端口)
- 调用
sd_notify(0, "READY=1") - systemd 将服务状态从
activating切换为active (running)
"READY=1" 表示服务已启动就绪(最关键!)
工作原理:
- systemd 在启动服务时,会设置一个 Unix domain socket 路径到环境变量
NOTIFY_SOCKET(如/run/systemd/notify)。 sd_notify()通过这个 socket 向 systemd 发送消息。- 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-sigint、shutdown-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