文章目录
客户端
服务器需要与多个客户端保持链接 。因此 Redis 在服务器端保存了与该服务器连接的所有客户端状态的结构。 redisCLient
/* With multiplexing we need to take per-client state.
* Clients are taken in a liked list.
*
* 因为 I/O 复用的缘故,需要为每个客户端维持一个状态。
*
* 多个客户端状态被服务器用链表连接起来。
*/
typedef struct redisClient {
// 套接字描述符
int fd;
// 当前正在使用的数据库
redisDb *db;
// 当前正在使用的数据库的 id (号码)
int dictid;
// 客户端的名字
robj *name; /* As set by CLIENT SETNAME */
// 查询缓冲区
sds querybuf;
// 查询缓冲区长度峰值
size_t querybuf_peak; /* Recent (100ms or more) peak of querybuf size */
// 参数数量
int argc;
// 参数对象数组
robj **argv;
// 记录被客户端执行的命令
struct redisCommand *cmd, *lastcmd;
// 请求的类型:内联命令还是多条命令
int reqtype;
// 剩余未读取的命令内容数量
int multibulklen; /* number of multi bulk arguments left to read */
// 命令内容的长度
long bulklen; /* length of bulk argument in multi bulk request */
// 回复链表
list *reply;
// 回复链表中对象的总大小
unsigned long reply_bytes; /* Tot bytes of objects in reply list */
// 已发送字节,处理 short write 用
int sentlen; /* Amount of bytes already sent in the current
buffer or object being sent. */
// 创建客户端的时间
time_t ctime; /* Client creation time */
// 客户端最后一次和服务器互动的时间
time_t lastinteraction; /* time of the last interaction, used for timeout */
// 客户端的输出缓冲区超过软性限制的时间
time_t obuf_soft_limit_reached_time;
// 客户端状态标志
int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
// 当 server.requirepass 不为 NULL 时
// 代表认证的状态
// 0 代表未认证, 1 代表已认证
int authenticated; /* when requirepass is non-NULL */
// 复制状态
int replstate; /* replication state if this is a slave */
// 用于保存主服务器传来的 RDB 文件的文件描述符
int repldbfd; /* replication DB file descriptor */
// 读取主服务器传来的 RDB 文件的偏移量
off_t repldboff; /* replication DB file offset */
// 主服务器传来的 RDB 文件的大小
off_t repldbsize; /* replication DB file size */
sds replpreamble; /* replication DB preamble. */
// 主服务器的复制偏移量
long long reploff; /* replication offset if this is our master */
// 从服务器最后一次发送 REPLCONF ACK 时的偏移量
long long repl_ack_off; /* replication ack offset, if this is a slave */
// 从服务器最后一次发送 REPLCONF ACK 的时间
long long repl_ack_time;/* replication ack time, if this is a slave */
// 主服务器的 master run ID
// 保存在客户端,用于执行部分重同步
char replrunid[REDIS_RUN_ID_SIZE+1]; /* master run id if this is a master */
// 从服务器的监听端口号
int slave_listening_port; /* As configured with: SLAVECONF listening-port */
// 事务状态
multiState mstate; /* MULTI/EXEC state */
// 阻塞类型
int btype; /* Type of blocking op if REDIS_BLOCKED. */
// 阻塞状态
blockingState bpop; /* blocking state */
// 最后被写入的全局复制偏移量
long long woff; /* Last write global replication offset. */
// 被监视的键
list *watched_keys; /* Keys WATCHED for MULTI/EXEC CAS */
// 这个字典记录了客户端所有订阅的频道
// 键为频道名字,值为 NULL
// 也即是,一个频道的集合
dict *pubsub_channels; /* channels a client is interested in (SUBSCRIBE) */
// 链表,包含多个 pubsubPattern 结构
// 记录了所有订阅频道的客户端的信息
// 新 pubsubPattern 结构总是被添加到表尾
list *pubsub_patterns; /* patterns a client is interested in (SUBSCRIBE) */
sds peerid; /* Cached peer ID. */
/* Response buffer */
// 回复偏移量
int bufpos;
// 回复缓冲区
char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;
该状态使用链表组织,因此对于客户端进行操作时,都由其遍历链表完成。
客户端状态包含的属性可以分为两类:
-
一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,它们都要用到这些属性。
-
另外一类是和特定功能相关的属性,比如操作数据库时需要用到的db属性和dictid属性,执行事务时需要用到的mstate属性,以及执行WATCH命令时需要用到的watched_ keys 属性等等。
客户端的一些通用属性
-
fd 套接字
伪客户端始终为 -1。
处理的命令请求来源是AOF或者Lua脚本。而不是网络
,所以这种客户端不需要套接字连接,自然也不需要记录套接字描述符。目前Redis服务器会在两个地方用到伪客户端,一个用于载人AOF文件并还原数据库状态,而另一个则用于执行Lua脚本中包含的Redis命令fd >-1 。普通客户端
-
客户端名字 robj *name;
-
客户端标志 int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI … */(主从复制等等)
-
输入缓冲区 sds querybuf; 保存客户端 发送的命令请求。
redis设计的TCP报文边界是以\r\n来划分的。
举个例子,如果客户端要向服务器发送以下命令请求:
SET msg “helloworld”
那么客户端实际发送的数据是:
*3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$11\r\nhelloworld\r\n输入缓冲区的大小会根据输入内容的大小动态变化。但是超过1GB,服务器就会关闭这个客户端。
-
命令与命令参数:将输入内容保存到 querybuf 中时,redis 就会解析其中的内容。保存到argc,argv。
// 参数数量
int argc;
// 参数对象数组
robj **argv; -
根据项argv[0]的值,在命令表中查找命令所对应的命令实现函数。之后将redisClient结构的cmd指向对应的命令表中的结构,最后通过 argc,argv,以及cmd来调用实现函数。
-
输出缓冲区:
执行命令后所得的命令回复会被保存在客户端状态的输出缓冲区里面
,每个客户端都有两个输出缓冲区可用,一个缓冲区的大小是固定的,另一个缓冲区的大小是可变的
:固定大小的缓冲区用于保存那些长度比较小的回复,比如OK、简短的字符串值、整数值、错误回复等等。 int bufpos;
char buf[REDIS_REPLY_CHUNK_BYTES]; 默认16KB可变大小的缓冲区用于保存那些长度比较大的回复,比如一个非常长的字符串值等,list *reply;
客户端的创建与关闭
客户端的创建
- 普通客户端的创建(略)
- lua伪客户端。初始化时创建,服务器关闭时关闭。
- AOF客户端。AOF载入完成之后关闭。
关闭普通客户端的时机
- 如果客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭。
- 如果客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端也会被服务器关闭。
- 如果客户端成为了CLIENT KILL命令的目标,那么它也会被关闭。
- 如果用户为服务器设置了timeout配置选项,那么当客户端的空转时间超过timeout选项设置的值时,客户端将被关闭
- 如果客户端发送的命令请求的大小超过了输人缓冲区的限制大小(默认为1GB)
- 如果要发送给客户端的命令回复的大小超过了输出缓冲区的限制大小,那么这个客户端会被服务器关闭。
对于可变大小的输出缓冲区的限制具体的规则是:
- 硬性限制( hard limit):如果输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器立即关闭客户端
- 软性限制( soft limit):如果输出缓冲区的大小超过了软性限制所设置的大小,但还没超过硬性限制,那么服务器将使用客户端状态结构的obuf_ soft_ limit_reached_time属性记录下客户端到达软性限制的起始时间;之后服务器会继续监视客户端,如果输出缓冲区的大小一直超出软性限制,并且持续时间超过服务器设定的时长,那么服务器将关闭客户端;相反地,如果输出缓冲区的大小在指定时间之内,不再超出软性限制,那么客户端就不会被关闭。obuf_ soft_ limit_reached_time也被清零
服务器
一个命令请求的流程
(1)发送命令请求(遵守redis协议,具体见:https://redis.io/topics/protocol
(2) 读取命令请求(输入缓冲区->命令解析->命令表查找与执行)
- 一些命令执行前的预备操作。(身份验证,maxmemory功能,监视器功能等。。。)
- 具体执行 (client->cmd->proc(client))
- 执行一些后续处理(AOF,慢查询,命令传播)
(3)输出缓冲区,命令回复处理器
(4)再次解析协议并打印
ServerCron 函数
Redis将serverCron作为时间事件来运行,从而确保它每隔一段时间就会自动运行一次,又因为serverCron需要在Redis服务器运行期间一直定期运行,所以它是一个循环时间事件: serverCron 会一直定期执行,直到服务器关闭为止。
负责管理服务器的资源,维持服务器的整体平衡。
更新服务器时间缓存
redis中有许多功能要获取系统当前时间,则需要调用系统接口查询时间,这样比较耗时,因此redis在结构体中用unixtime、mstime属性,保存了当前时间,并且定时更新这个值。作为对于时间的一个缓存。对于键过期时间、慢查询日志等,服务器会再次进行系统时间调用,获取最精确的时间。
更新服务器lru时间(回忆对象的空转时长的计算)
更新服务器每秒执行命令数
更新服务器内存峰值
redis服务器中,用stat_peak_memory记录服务器内存峰值。
处理sigterm信号
redis服务器,用属性shutdown_asap记录当前的结果,0是不用进行操作,1的话是要求服务器尽快关闭。
因此,服务器关闭命令shutdown执行,并不会立即关闭服务器,而是将服务器的shutdown_asap属性置成1,当下一次serverCron读取时,就会拒绝新的请求,完成当前正在执行的命令后,开始持久化相关的操作,结束持久化后才会关闭服务器。
管理客户端资源(回忆上面提到的客户端关闭的几种情况)
管理数据库资源
主要是检查键是否过期,并且按照配置的策略,删除过期的键。如懒惰删除、定期删除等。
检查持久化操作的运行状态
将aof缓冲区内容写入aof文件
如果开启aof,redis会记录每个写命令,写入aof缓冲区,但是为了减少磁盘I/O,不会立即写入aof文件。而是在执行serverCron函数时,才会开始将缓冲区内容写入aof文件。
记录执行一次serverCron
redis用属性cronloops保存serverCron函数执行的次数。当执行一次serverCron,则会将属性值加1。这个值目前的作用,是在主从复制情况下,会有一个条件是,每执行n次serverCron,则执行一次指定代码。(这里其实需要探究一下具体是执行什么代码)
服务器的启动流程
主要有五个步骤,如下:
- 1、初始化状态结构
首先,会创建一个struct redisServer实例变量,存储服务器的状态。
接着,redis初始化服务器,会执行一次redis.c/initServerConfig函数,主要工作是设置服务器运行ID、默认运行频率、默认配置文件路径、运行架构、默认端口号、RDB条件、AOF条件、LRU时钟、创建命令表。
初始化状态结构,都是简单的结构,后续的数据库、共享对象、慢查询日志、Lua环境等,都是后面才创建的。
- 2、载入配置选项
在启动redis服务器时,可以通过参数指定配置文件、端口号等。redis会载入这些配置,并且和默认不同的时候,会覆盖默认的配置。
例如输入redis-server –port5000,则会先创建端口基于6379的,再在这一步修改端口号为5000。
在加载用户配置的文件,如果有定义新的结果,则使用新结果,否则就使用默认值。
-
3、初始化服务器数据结构
- 1)创建数据结构
在第一步,只创建了一个命令表,在此步骤则会创建其他数据结构,
包括:
server.client //链表,用于存储普通客户端,每个节点是一个redisClient结构;
server.db //链表,保存所有的数据库;
server.pubsub_channels//链表,保存频道订阅信息;server.pubsub_patterns链表,
//保存模式订阅信息。
server.lua //用于执行lua脚本的环境。
server.showlog //用于保存慢查询。
服务器会为上述结构分配内存空间。在此步骤才创建数据结构,是因为如果第一步创建,而第二步加载用户自定义配置的时候,有可能会修改到某些内容,则还需要重写。而命令表由于是固定的,因此可以放到第一步创建。
- 2)其他设置操作
除了创建数据结构,还会进行一些重要的设置。
包括:
为服务器设置进程信号处理器。
创建共享对象,包括整数1~10000的字符串对象,“OK”、“ERR”回复的字符串对象等,
用于避免反复创建相同对象。
打开服务器监听端口,为监听的套接字添加相应的应答事件,等待服务器正式运行时接收客户端的连接。
为serverCron函数创建时间事件,等待服务器正式执行serverCron。
如果AOF持久化开启,则打开aof文件,如果不存在则创建aof文件。
初始化服务器后台I/O模块(bio),为将来的I/O做好准备。
- 4、还原数据库状态
如果开启aof,则载入aof文件;如果没有开启aof,则载入rdb文件。
载入完成后,在日志中打印载入的耗时。
- 5、执行事件循环
初始化最后一步,服务器将打印连接成功的日志。并且开始事件循环,初始化正式完成,可以开始处理客户端的请求。