Redis服务器是典型的一对多服务器程序:一个服务器与多个客户端建立网络连接,每个可互段可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。
通过使用由I/O多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式处理命令请求,并与多个客户端进行网络通信。对于每个与服务器连接的客户端,服务器都为这些客户端建立了相应的redisClient结构(客户端状态),这个结构保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构。
目录
Redis客户端
客户端属性
客户端的创建与关闭
总结
Redis服务器
命令请求的执行过程
serverCron 函数
服务器初始化
总结
客户端状态包含的属性可以分为两类:
db
属性和 dictid
属性, 执行事务时需要用到的 mstate
属性, 以及执行 WATCH 命令时需要用到的 watched_keys
属性, 等等。1、套接字描述符
typedef struct redisClient {
// ...
//套接字描述符
int fd;
// ...
} redisClient;
客户端状态的 fd
属性记录了客户端正在使用的套接字描述符,根据客户端类型的不同,fd
属性的值也不同:
fd
属性的值为 -1
: 伪客户端处理的命令请求来源于 AOF 文件或者 Lua 脚本, 而不是网络, 所以这种客户端不需要套接字连接, 自然也不需要记录套接字描述符。 目前 Redis 服务器会在两个地方用到伪客户端, 一个用于载入 AOF 文件并还原数据库状态, 而另一个则用于执行 Lua 脚本中包含的 Redis 命令。fd
属性的值为大于 -1
的整数: 普通客户端使用套接字来与服务器进行通讯,服务器会用 fd
属性来记录客户端套接字的描述符。 因为合法的套接字描述符不能是 -1
,所以普通客户端的套接字描述符的值必然是大于 -1
的整数。2、名字和标志
在默认情况下, 一个连接到服务器的客户端是没有名字的。使用 CLIENT_SETNAME 命令可以为客户端设置一个名字, 让客户端的身份变得更清晰。
客户端的名字记录在客户端状态的 name
属性里面,如果客户端没有为自己设置名字,那么相应客户端状态的 name
属性指向NULL指针; 如果客户端为自己设置了名字, 那么 name
属性将指向一个字符串对象, 而该对象就保存着客户端的名字。
客户端的标志属性 flags 记录了客户端的角色(role),以及客户端目前所处的状态:
//flags 属性的值可以是单个标志
flags =
//也可以是多个标志的二进制
flags = | | ...
每个标志使用一个常量表示, 一部分标志记录了客户端的角色:
- 在主从服务器进行复制操作时, 主服务器会成为从服务器的客户端, 而从服务器也会成为主服务器的客户端。
REDIS_MASTER
标志表示客户端代表的是一个主服务器,REDIS_SLAVE
标志表示客户端代表的是一个从服务器。REDIS_PRE_PSYNC
标志表示客户端代表的是一个版本低于 Redis 2.8 的从服务器, 主服务器不能使用 PSYNC 命令与这个从服务器进行同步。 这个标志只能在REDIS_SLAVE
标志处于打开状态时使用。REDIS_LUA_CLIENT
标识表示客户端是专门用于处理 Lua 脚本里面包含的 Redis 命令的伪客户端。
而另外一部分标志则记录了客户端目前所处的状态:
REDIS_MONITOR
标志表示客户端正在执行 MONITOR 命令。REDIS_UNIX_SOCKET
标志表示服务器使用 UNIX 套接字来连接客户端。REDIS_BLOCKED
标志表示客户端正在被 BRPOP 、 BLPOP 等命令阻塞。REDIS_UNBLOCKED
标志表示客户端已经从REDIS_BLOCKED
标志所表示的阻塞状态中脱离出来, 不再阻塞。REDIS_UNBLOCKED
标志只能在REDIS_BLOCKED
标志已经打开的情况下使用。REDIS_MULTI
标志表示客户端正在执行事务。REDIS_DIRTY_CAS
标志表示事务使用 WATCH 命令监视的数据库键已经被修改,REDIS_DIRTY_EXEC
标志表示事务在命令入队时出现了错误, 以上两个标志都表示事务的安全性已经被破坏, 只要这两个标记中的任意一个被打开, EXEC 命令必然会执行失败。 这两个标志只能在客户端打开了REDIS_MULTI
标志的情况下使用。REDIS_CLOSE_ASAP
标志表示客户端的输出缓冲区大小超出了服务器允许的范围, 服务器会在下一次执行serverCron
函数时关闭这个客户端, 以免服务器的稳定性受到这个客户端影响。 积存在输出缓冲区中的所有内容会直接被释放, 不会返回给客户端。REDIS_CLOSE_AFTER_REPLY
标志表示有用户对这个客户端执行了 CLIENT_KILL 命令, 或者客户端发送给服务器的命令请求中包含了错误的协议内容。 服务器会将客户端积存在输出缓冲区中的所有内容发送给客户端, 然后关闭客户端。REDIS_ASKING
标志表示客户端向集群节点(运行在集群模式下的服务器)发送了 ASKING 命令。REDIS_FORCE_AOF
标志强制服务器将当前执行的命令写入到 AOF 文件里面,REDIS_FORCE_REPL
标志强制主服务器将当前执行的命令复制给所有从服务器。 执行 PUBSUB 命令会使客户端打开REDIS_FORCE_AOF
标志, 执行 SCRIPT_LOAD 命令会使客户端打开REDIS_FORCE_AOF
标志和REDIS_FORCE_REPL
标志。- 在主从服务器进行命令传播期间, 从服务器需要向主服务器发送 REPLICATION ACK 命令, 在发送这个命令之前, 从服务器必须打开主服务器对应的客户端的
REDIS_MASTER_FORCE_REPLY
标志, 否则发送操作会被拒绝执行。
flags
属性的例子:
# 客户端是一个主服务器
REDIS_MASTER
# 客户端正在被列表命令阻塞
REDIS_BLOCKED
# 客户端正在执行事务,但事务的安全性已被破坏
REDIS_MULTI | REDIS_DIRTY_CAS
# 客户端是一个从服务器,并且版本低于 Redis 2.8
REDIS_SLAVE | REDIS_PRE_PSYNC
# 这是专门用于执行 Lua 脚本包含的 Redis 命令的伪客户端
# 它强制服务器将当前执行的命令写入 AOF 文件,并复制给从服务器
REDIS_LUA_CLIENT | REDIS_FORCE_AOF | REDIS_FORCE_REPL
3、输入缓冲区
客户端状态的输入缓冲区用于保存客户端发送的命令请求:
typedef struct redisClient {
// ...
sds querybuf;
// ...
} redisClient;
输入缓冲区的大小会根据输入内容动态地缩小或者扩大, 但它的最大大小不能超过 1 GB , 否则服务器将关闭这个客户端。
在服务器将客户端发送的命令请求保存到客户端状态的 querybuf
属性之后, 服务器将对命令请求的内容进行分析, 并将得出的命令参数以及命令参数的个数分别保存到客户端状态的 argv
属性和 argc
属性:
argv
属性是一个数组,数组中的每个项都是一个字符串对象:其中 argv[0]
是要执行的命令,而之后的其他项则是传给命令的参数。argc
属性则负责记录 argv
数组的长度。4、命令实现
当服务器从协议内容中分析并得出 argv
属性和 argc
属性的值之后, 服务器将根据项 argv[0]
的值, 在命令表中查找命令所对应的命令实现函数。
命令表是一个字典, 字典的键是一个 SDS 结构, 保存了命令的名字, 字典的值是命令所对应的 redisCommand
结构, 这个结构保存了命令的实现函数、 命令的标志、 命令应该给定的参数个数、 命令的总执行次数和总消耗时长等统计信息。
当程序在命令表中成功找到 argv[0]
所对应的 redisCommand
结构时, 它会将客户端状态的 cmd
指针指向这个结构:
typedef struct redisClient {
// ...
struct redisCommand *cmd;
// ...
} redisClient;
之后, 服务器就可以使用 cmd
属性所指向的 redisCommand
结构, 以及 argv
、 argc
属性中保存的命令参数信息, 调用命令实现函数, 执行客户端指定的命令。
5、输出缓冲区
执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面, 每个客户端都有两个输出缓冲区可用, 一个缓冲区的大小是固定的, 另一个缓冲区的大小是可变的:
OK
、简短的字符串值、整数值、错误回复,等等。客户端的固定大小缓冲区由 buf
和 bufpos
两个属性组成:
typedef struct redisClient {
// ...
char buf[REDIS_REPLY_CHUNK_BYTES];
int bufpos;
// ...
} redisClient;
buf
是一个大小为 REDIS_REPLY_CHUNK_BYTES
字节的字节数组, 而 bufpos
属性则记录了 buf
数组目前已使用的字节数量。
REDIS_REPLY_CHUNK_BYTES
常量目前的默认值为 16*1024
, 也即是说, buf
数组的默认大小为 16 KB 。
可变大小缓冲区由 reply
链表和一个或多个字符串对象组成:
6、身份验证
客户端状态的 authenticated
属性用于记录客户端是否通过了身份验证:
typedef struct redisClient {
// ...
int authenticated;
// ...
} redisClient;
如果 authenticated
的值为 0
, 那么表示客户端未通过身份验证; 如果 authenticated
的值为 1
, 那么表示客户端已经通过了身份验证。
当客户端 authenticated
属性的值为 0
时, 除了 AUTH 命令之外, 客户端发送的所有其他命令都会被服务器拒绝执行,当客户端通过 AUTH 命令成功进行身份验证之后, 客户端状态 authenticated
属性的值就会从 0
变为 1
。
authenticated
属性仅在服务器启用了身份验证功能时使用: 如果服务器没有启用身份验证功能的话, 那么即使authenticated
属性的值为 0
(这是默认值), 服务器也不会拒绝执行客户端发送的命令请求。
创建普通客户端
如果客户端是通过网络连接与服务器进行连接的普通客户端,那么客户端使用 connect 函数连接服务器时,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾。
关闭普通客户端
客户端关闭有下述情况:
服务器通过两种方式来先知客户端输出缓冲区的大小:
伪客户端
Lua伪客户端,不是存放在redisServer的clients链表中,而是单独有一个redisClient类型的属性lua_client,用于存放redis的lua伪客户端。该客户端创建后,不会关闭,直到服务器关闭才会关闭。
AOF伪客户端,服务器载入aof文件时,会创建aof伪客户端,并且载入完毕后关闭该客户端。
clients
链表连接起多个客户端状态, 新添加的客户端状态会被放到链表的末尾。flags
属性使用不同标志来表示客户端的角色, 以及客户端当前所处的状态。argv
和 argc
属性里面, 而 cmd
属性则记录了客户端要执行命令的实现函数。Redis服务器负责与多个客户端建立网络连接,处理客户端发来的命令请求,在数据库中保存客户端执行命令所产生的数据并通过资源管理来维持服务器自身的运转。
一个命令请求从发送到获得回复的过程中, 客户端和服务器需要完成一系列操作。
举个例子, 如果我们使用客户端执行以下命令: SET KEY VALUE
那么从客户端发送 SET KEY VALUE
命令到获得回复 OK
期间, 客户端和服务器共需要执行以下操作:
SET KEY VALUE
。SET KEY VALUE
, 在数据库中进行设置操作, 并产生命令回复 OK
。OK
发送给客户端。OK
, 并将这个回复打印给用户观看。1、发送命令请求
Redis 服务器的命令请求来自 Redis 客户端, 当用户在客户端中键入一个命令请求时, 客户端会将这个命令请求转换成协议格式, 然后通过连接到服务器的套接字, 将协议格式的命令请求发送给服务器。
2、读取命令请求
当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时, 服务器将调用命令请求处理器来执行以下操作:
argv
属性和 argc
属性里面。3、命令执行器
(1)查找
命令执行器要做的第一件事就是根据客户端状态的 argv[0]
参数,在命令表(command table)中查找参数所指定的命令, 并将找到的命令保存到客户端状态的 cmd
属性里面。
属性名 | 类型 | 作用 |
---|---|---|
name |
char * |
命令的名字,比如 "set" 。 |
proc |
redisCommandProc * |
函数指针,指向命令的实现函数,比如 setCommand 。 redisCommandProc 类型的定义为typedef void redisCommandProc(redisClient *c); 。 |
arity |
int |
命令参数的个数,用于检查命令请求的格式是否正确。 如果这个值为负数 -N ,那么表示参数的数量大于等于 N 。 注意命令的名字本身也是一个参数, 比如说 SET msg "helloworld" 命令的参数是 "SET" 、 "msg" 、 "hello world" , 而不仅仅是 "msg" 和 "hello world" 。 |
sflags |
char * |
字符串形式的标识值, 这个值记录了命令的属性, 比如这个命令是写命令还是读命令, 这个命令是否允许在载入数据时使用, 这个命令是否允许在 Lua 脚本中使用, 等等。 |
flags |
int |
对 sflags 标识进行分析得出的二进制标识, 由程序自动生成。 服务器对命令标识进行检查时使用的都是 flags 属性而不是 sflags 属性, 因为对二进制标识的检查可以方便地通过 & 、 ^ 、 ~ 等操作来完成。 |
calls |
long long |
服务器总共执行了多少次这个命令。 |
milliseconds |
long long |
服务器执行这个命令所耗费的总时长。 |
需要注意的是,因为命令表使用的是大小写无关的查找算法,所以命令名字的大小写不影响命令表的查找结果。
(2)执行预备
到目前为止, 服务器已经将执行命令所需的命令实现函数(保存在客户端状态的 cmd
属性)、参数(保存在客户端状态的 argv
属性)、参数个数(保存在客户端状态的 argc
属性)都收集齐了, 但是在真正执行命令之前, 程序还需要进行一些预备操作, 从而确保命令可以正确、顺利地被执行, 这些操作包括:
- 检查客户端状态的
cmd
指针是否指向NULL
, 如果是的话, 那么说明用户输入的命令名字找不到相应的命令实现, 服务器不再执行后续步骤, 并向客户端返回一个错误。- 根据客户端
cmd
属性指向的redisCommand
结构的arity
属性, 检查命令请求所给定的参数个数是否正确, 当参数个数不正确时, 不再执行后续步骤, 直接向客户端返回一个错误。 比如说, 如果redisCommand
结构的arity
属性的值为-3
, 那么用户输入的命令参数个数必须大于等于3
个才行。- 检查客户端是否已经通过了身份验证, 未通过身份验证的客户端只能执行 AUTH 命令, 如果未通过身份验证的客户端试图执行除 AUTH 命令之外的其他命令, 那么服务器将向客户端返回一个错误。
- 如果服务器打开了
maxmemory
功能, 那么在执行命令之前, 先检查服务器的内存占用情况, 并在有需要时进行内存回收, 从而使得接下来的命令可以顺利执行。 如果内存回收失败, 那么不再执行后续步骤, 向客户端返回一个错误。- 如果服务器上一次执行 BGSAVE 命令时出错, 并且服务器打开了
stop-writes-on-bgsave-error
功能, 而且服务器即将要执行的命令是一个写命令, 那么服务器将拒绝执行这个命令, 并向客户端返回一个错误。- 如果客户端当前正在用 SUBSCRIBE 命令订阅频道, 或者正在用 PSUBSCRIBE 命令订阅模式, 那么服务器只会执行客户端发来的 SUBSCRIBE 、 PSUBSCRIBE 、 UNSUBSCRIBE 、 PUNSUBSCRIBE 四个命令, 其他别的命令都会被服务器拒绝。
- 如果服务器正在进行数据载入, 那么客户端发送的命令必须带有
l
标识(比如 INFO 、 SHUTDOWN 、 PUBLISH ,等等)才会被服务器执行, 其他别的命令都会被服务器拒绝。- 如果服务器因为执行 Lua 脚本而超时并进入阻塞状态, 那么服务器只会执行客户端发来的 SHUTDOWN nosave 命令和 SCRIPT KILL 命令, 其他别的命令都会被服务器拒绝。
- 如果客户端正在执行事务, 那么服务器只会执行客户端发来的 EXEC 、 DISCARD 、 MULTI 、 WATCH 四个命令, 其他命令都会被放进事务队列中。
- 如果服务器打开了监视器功能, 那么服务器会将要执行的命令和参数等信息发送给监视器。
- 以上只列出了服务器在单机模式下执行命令时的检查操作, 当服务器在复制或者集群模式下执行命令时, 预备操作还会更多一些。
(3)调用命令的实现函数
在前面的操作中, 服务器已经将要执行命令的实现保存到了客户端状态的 cmd
属性里面, 并将命令的参数和参数个数分别保存到了客户端状态的 argv
属性和 argc
属性里面, 当服务器决定要执行命令时, 它只要执行以下语句就可以了:
// client 是指向客户端状态的指针
client->cmd->proc(client);
因为执行命令所需的实际参数都已经保存到客户端状态的 argv
属性里面了, 所以命令的实现函数只需要一个指向客户端状态的指针作为参数即可。
被调用的命令实现函数会执行指定的操作, 并产生相应的命令回复, 这些回复会被保存在客户端状态的输出缓冲区里面(buf
属性和 reply
属性), 之后实现函数还会为客户端的套接字关联命令回复处理器, 这个处理器负责将命令回复返回给客户端。
(4)后续工作
在执行完实现函数之后, 服务器还需要执行一些后续工作:
redisCommand
结构的 milliseconds
属性, 并将命令的 redisCommand
结构的 calls
计数器的值增一。当以上操作都执行完了之后, 服务器对于当前命令的执行到此就告一段落了, 之后服务器就可以继续从文件事件处理器中取出并处理下一个命令请求了
4、将命令回复返回客户端
前面说过, 命令实现函数会将命令回复保存到客户端状态的输出缓冲区里面,并为客户端的套接字关联命令回复处理器,当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。
当命令回复发送完毕之后, 回复处理器会清空客户端状态的输出缓冲区, 为处理下一个命令请求做好准备。
当客户端接收到协议格式的命令回复之后, 它会将这些回复转换成人类可读的格式, 并打印给用户观看。
Redis服务器中,默认情况下,serverCron函数每100毫秒执行一次,这个执行间隔可以在配置文件进行设置。这个函数是用于管理服务器的资源,保证服务器更良好的运转。
1、更新服务器时间缓存
redis中有许多功能要获取系统当前时间,则需要调用系统接口查询时间,这样比较耗时,因此redis在结构体中用unixtime、mstime属性,保存了当前时间,并且定时更新这个值。前者是秒级unix时间戳,后者是毫秒级unix时间戳。
2、更新lru时间
lru记录的是服务器最后一次被访问的时间,是用于服务器的计算空转时长,用属性lruclock进行存储。默认情况下,每10秒更新一次。另外,每个redis对象也存了一个lru,保存的是该对象最后一次被被访问的时间。
当要计算redis对象的空转时间,则会用服务器的lru减去redis对象的lru,获得的结果即对象的空转时长。
在redis客户端,用命令objectidletime key,可以查看该key的空转时长,返回结果是以秒为单位。由于redis每10秒更新一次服务器的最后访问时间,因此不是很精确。
3、更新服务器每秒执行命令数
这个不是通过扫描全部的键,而是采用抽样的方式确定的结果。每100毫秒1次,随机抽取一些键,查看最近1秒是否有操作,来确定最近1秒的操作次数。
接着,会将这个值,与上一次的结果,取平均值,作为本次计算的每秒执行命令数。在存入结构体中,供下次取平均值使用。
4、更新服务器内存峰值
redis服务器中,用stat_peak_memory记录服务器内存峰值。每次执行serverCron函数,会查看当前内存使用量,并且与stat_peak_memory比较,如果超过这个值,就更新这个属性。
5、处理sigterm信号
redis服务器,用属性shutdown_asap记录当前的结果,0是不用进行操作,1的话是要求服务器尽快关闭。
因此,服务器关闭命令shutdown执行,并不会立即关闭服务器,而是将服务器的shutdown_asap属性置成1,当下一次serverCron读取时,就会拒绝新的请求,完成当前正在执行的命令后,开始持久化相关的操作,结束持久化后才会关闭服务器。
6、管理客户端资源
主要是会检查客户端的以下内容:
7、管理数据库资源
主要是检查键是否过期,并且按照配置的策略,删除过期的键。如懒惰删除、定期删除等。
8、执行被延迟的bgrewriteaof命令
redis用属性aof_rewrite_scheduled记录是否有延迟的bgrewriteaof命令。
当执行bgsave命令期间,如果接收到bgrewriteaof命令,不会立即执行该命令,而是会将属性aof_rewrite_scheduled置成1。
每次执行serverCron函数执行时,发现属性aof_rewrite_scheduled是1,会检查当前是否在执行bgsave命令或bgrewriteaof命令,如果没有在执行这两个命令,则会执行bgrewriteaof命令。
9、检查持久化操作的运行状态
redis服务器分别用rdb_child_pid和aof_child_pid属性,记录RDB和AOF的子进程号(即子进程pid),如果没有在执行相应的持久化,则值是-1。
有一个值不是-1时:每次服务器检查这两个属性,发现有一个不是-1,则会检查子进程是否有信号发来服务器进程。
如果有信号,表示rdb完成或aof重写完毕,服务器会进行后续的操作,比如用新的rdb、aof替换旧的相应文件。
如果没信号,表示持久化还没完成,程序不做动作。
两个值都是-1时:两个值都不是-1,会进行三个检查:
- 如果bgrewriteaof命令有存在延迟(即上述aof_rewrite_scheduled值是1),因为两个属性都是 -1,表示当前没有在持久化,则redis服务器会开始aof的重写。
- 检查服务器是否满足bgsave条件,如果满足,因为两个属性都是 -1,则会开始执行bgsave。
- 检查服务器是否满足bgrewriteaof条件,如果满足,因为两个属性都是 -1,则会开始执行bgrewriteaof。
10、将aof缓冲区内容写入AOF文件
如果开启aof,redis会记录每个写命令,写入aof缓冲区,但是为了减少磁盘I/O,不会立即写入aof文件。而是在执行serverCron函数时,才会开始将缓冲区内容写入aof文件。
11、记录执行一次serverCron
redis用属性cronloops保存serverCron函数执行的次数。当执行一次serverCron,则会将属性值加1。
这个值目前的作用,是在主从复制情况下,会有一个条件是,每执行n次serverCron,则执行一次指定代码。
redis服务器开启时,会先进行初始化,主要有五个步骤。
1、初始化状态结构
首先,会创建一个struct redisServer实例变量,存储服务器的状态,并为结构中的各个属性设置默认值。
接着,redis初始化服务器,会执行一次redis.c/initServerConfig函数,主要工作是设置服务器运行ID、默认运行频率、默认配置文件路径、运行架构、默认端口号、RDB条件、AOF条件、LRU时钟、创建命令表。
初始化状态结构,都是简单的结构,后续的数据库、共享对象、慢查询日志、Lua环境等,都是后面才创建的。
2、载入配置选项
在启动redis服务器时,可以通过给定配置参数和指定配置文件来修改服务器的默认配置。redis会载入这些配置,并且在和默认不同的时候,会覆盖默认的配置。
例如输入redis-server –port 5000,则会先创建端口基于6379的,再在这一步修改端口号为5000。
在加载用户配置的文件,如果有定义新的结果,则使用新结果,否则就使用默认值。
3、初始化服务器数据结构
1)创建数据结构
在第一步,只创建了一个命令表,在此步骤则会创建其他数据结构,
server.client //链表,用于存储普通客户端,每个节点是一个redisClient结构;
server.db //链表,保存所有的数据库;
server.pubsub_channels//链表,保存频道订阅信息;server.pubsub_patterns链表,保存模式订阅信息。
server.lua //用于执行lua脚本的环境。
server.showlog //用于保存慢查询。
服务器会为上述结构分配内存空间。在此步骤才创建数据结构,是因为如果第一步创建,而第二步加载用户自定义配置的时候,有可能会修改到某些内容,则还需要重写。而命令表由于是固定的,因此可以放到第一步创建。
2)其他设置操作
除了创建数据结构,还会进行一些重要的设置。包括:
4、还原数据库状态
如果开启aof,则载入aof文件;如果没有开启aof,则载入rdb文件。
载入完成后,在日志中打印载入的耗时。
5、执行事件循环
初始化最后一步,服务器将打印连接成功的日志。并且开始事件循环,初始化正式完成,可以开始处理客户端的请求。
serverCron
函数默认每隔 100
毫秒执行一次, 它的工作主要包括更新服务器状态信息, 处理服务器接收的 SIGTERM
信号, 管理客户端资源和数据库状态, 检查并执行持久化操作, 等等。
参考文章:
《Redis设计与实现》