Redis学习教程之命令的执行过程详解
前言
之前写了一系列文章,已经很深入的探讨了 redis 的数据结构,数据库的实现,key的过期策略以及 redis 是怎么处理事件的。所以距离 redis 的单机实现只差最后一步了,就是 redis 是怎么处理 client 发来的命令并返回结果的,所以我们就仔细讨论一下 redis 是怎么执行命令的。
阅读这篇文章你将会了解到:
- redis 是怎么执行远程客户端发来的命令的
redis client(客户端)
redis 是单线程应用,它是如何与多个客户端简历网络链接并处理命令的?
由于 redis 是基于 i/o 多路复用技术,为了能够处理多个客户端的请求,redis 在本地为每一个链接到 redis 服务器的客户端创建了一个 redisclient 的数据结构,这个数据结构包含了每个客户端各自的状态和执行的命令。 redis 服务器使用一个链表来维护多个 redisclient 数据结构。
在服务器端用一个链表来管理所有的 redisclient。
struct redisserver { //... list *clients; /* list of active clients */ //... }
所以我就看看 redisclient 包含的数据结构和重要参数:
typedef struct redisclient { // 客户端状态标志 int flags; /* redis_slave | redis_monitor | redis_multi ... */ // 套接字描述符 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. */ // 回复偏移量 int bufpos; // 回复缓冲区 char buf[redis_reply_chunk_bytes]; // ... }
这里需要特别的注意,redisclient 并非指远程的客户端,而是一个 redis 服务本地的数据结构,我们可以理解这个 redisclient 是远程客户端的一个映射或者代理。
flags
flags 表示了目前客户端的角色,以及目前所处的状态。他比较特殊可以单独表示一个状态或者多个状态。
querybuf
querybuf 是一个 sds 动态字符串类型,所谓 buf 说明是它只是一个缓冲区,用于存储没有被解析的命令。
argc & argv
上文的 querybuf 是一个没有处理过的命令,当 redis 将 querybuf 命令解析以后,会将得出的参数个数和以及参数分别保存在 argc 和 argv 中。argv 是一个 redisobject 的数组。
cmd
redis 使用一个字典保存了所有的 rediscommand。key 是 rediscommand 的名字,值就是一个 rediscommand 结构,这个结构保存了命令的实现函数,命令的标志,命令应该给定的参数个数,命令的执行次数和总消耗时长等统计信息,cmd 是一个 rediscommand。
当 redis 解析出 argv 和 argc 后,会根据数组 argv[0],到字典中查询出对应的 rediscommand。上文的例子中 redis 就会去字典去查找 set 这个命令对应的 rediscommand。redis 会执行 rediscommand 中命令的实现函数。
buf & bufpos & reply
buf 是一个长度为 redis_reply_chunk_bytes 的数组。redis 执行相应的操作以后,就会将需要返回的返回的数据存储到 buf 中,bufpos 用于记录 buf 中已用的字节数数量,当需要恢复的数据大于 redis_reply_chunk_bytes 时,redis 就会是用 reply 这个链表来保存数据。
其他参数
其他参数大家看注释就能明白,就是字面的意思。省略的参数基本上涉及 redis 集群管理的参数,在之后的文章中会继续讲解。
客户端的链接和断开
上文说过 redisserver 是用一个链表来维护所有的 redisclient 状态,每当有一个客户端发起链接以后,就会在 redis 中生成一个对应的 redisclient 数据结构,增加到clients这个链表之后。
一个客户端很可能被多种原因断开。
总体分为几种类型:
- 客户端主动退出或者被 kill。
- timeout 超时。
- redis 为了自我保护,会断开发的数据超过限制大小的客户端。
- redis 为了自我保护,会断需要返回的数据超过限制大小的客户端。
调用总结
当客户端和服务器端的嵌套字变得可读的时候,服务器将会调用命令请求处理器来执行以下操作:
- 读取嵌套字中的数据,写入 querybuf。
- 解析 querybuf 中的命令,记录到 argc 和 argv 中。
- 根据 argv[0] 查找对应的 recommand。
- 执行 recommand 对应的实现函数。
- 执行以后将结果存入 buf & bufpos & reply 中,返回给调用方。
redis server (服务端)
上文是从 redisclient 的角度来观察命令的执行,文章接下来的部分将会从 redis 的代码层面,微观的观察 redis 是怎么实现命令的执行的。
redisserver 的启动
在了解redisserver 的工作机制的工作机制之前,需要了解 redisserver 的启动做了什么:
可以继续观察 redis 的 main() 函数。
int main(int argc, char **argv) { //... // 创建并初始化服务器数据结构 initserver(); //... }
我们只关注 initserver() 这个函数,他负责初始化服务器的数据结构。继续跟踪代码:
void initserver() { //... //创建eventloop server.el = aecreateeventloop(server.maxclients+redis_eventloop_fdset_incr); /* create an event handler for accepting new connections in tcp and unix * domain sockets. */ // 为 tcp 连接关联连接应答(accept)处理器 // 用于接受并应答客户端的 connect() 调用 for (j = 0; j < server.ipfd_count; j++) { if (aecreatefileevent(server.el, server.ipfd[j], ae_readable, accepttcphandler,null) == ae_err) { redispanic( "unrecoverable error creating server.ipfd file event."); } } // 为本地套接字关联应答处理器 if (server.sofd > 0 && aecreatefileevent(server.el,server.sofd,ae_readable, acceptunixhandler,null) == ae_err) redispanic("unrecoverable error creating server.sofd file event."); //... }
篇幅限制,我们省略了很多与本编文章无关的代码,保留了核心逻辑代码。
在上一篇文章中 《redis 中的事件驱动模型》 我们讲解过,redis 使用不同的事件处理器,处理不同的事件。
在这段代码里面:
- 初始化了事件处理器的 eventloop
- 向 eventloop 中注册了两个事件处理器 accepttcphandler 和 acceptunixhandler,分别处理远程的链接和本地链接。
redisclient 的创建
当有一个远程客户端连接到 redis 的服务器,会触发 accepttcphandler 事件处理器.
accepttcphandler 事件处理器,会创建一个链接。然后继续调用 acceptcommonhandler。
acceptcommonhandler 事件处理器的作用是:
- 调用 createclient() 方法创建 redisclient
- 检查已经创建的 redisclient 是否超过 server 允许的数量的上限
- 如果超过上限就拒绝远程连接
- 否则创建 redisclient 创建成功
- 并更新连接的统计次数,更新 redisclinet 的 flags 字段
这个时候 redis 在服务端创建了 redisclient 数据结构,这个时候远程的客户端就在 redisserver 中创建了一个代理。远程的客户端就与 redis 服务器建立了联系,就可以向服务器发送命令了。
处理命令
在 createclient() 行数中:
// 绑定读事件到事件 loop (开始接收命令请求) if (aecreatefileevent(server.el,fd,ae_readable,readqueryfromclient, c) == ae_err)
向 eventloop 中注册了 readqueryfromclient。 readqueryfromclient 的作用就是从client中读取客户端的查询缓冲区内容。
然后调用函数 processinputbuffer 来处理客户端的请求。在 processinputbuffer 中有几个核心函数:
- processinlinebuffer 和 processmultibulkbuffer 解析 querybuf 中的命令,记录到 argc 和 argv 中。
- processcommand 根据 argv[0] 查找对应的 recommen,执行 recommend 对应的执行函数。在执行之前还会验证命令的正确性。将结果存入 buf & bufpos & reply 中
返回数据
万事具备了,执行完了命令就需要把数据返回给远程的调用方。调用链如下
processcommand -> addreply -> prepareclienttowrite
在 prepareclienttowrite 中我们有见到了熟悉的代码:
aecreatefileevent(server.el, c->fd, ae_writable,sendreplytoclient, c) == ae_err) return redis_err;
向 eventloop 绑定了 sendreplytoclient 事件处理器。
在 sendreplytoclient 中观察代码发现,如果 bufpos 大于 0,将会把 buf 发送给远程的客户端,如果链表 reply 的长度大于0,就会将遍历链表 reply,发送给远程的客户端,这里需要注意的是,为了避免 reply 数据量过大,就会过度的占用资源引起 redis 相应慢。为了解决这个问题,当写入的总数量大于 redis_max_write_per_event 时,redis 将会临时中断写入,记录操作的进度,将处理时间让给其他操作,剩余的内容等下次继续。这样的套路我们一路走来看过太多了。
总结
- 远程客户端连接到 redis 后,redis服务端会为远程客户端创建一个 redisclient 作为代理。
- redis 会读取嵌套字中的数据,写入 querybuf 中。
- 解析 querybuf 中的命令,记录到 argc 和 argv 中。
- 根据 argv[0] 查找对应的 recommand。
- 执行 recommend 对应的执行函数。
- 执行以后将结果存入 buf & bufpos & reply 中。
- 返回给调用方。返回数据的时候,会控制写入数据量的大小,如果过大会分成若干次。保证 redis 的相应时间。
redis 作为单线程应用,一直贯彻的思想就是,每个步骤的执行都有一个上限(包括执行时间的上限或者文件尺寸的上限)一旦达到上限,就会记录下当前的执行进度,下次再执行。保证了 redis 能够及时响应不发生阻塞。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。