转载请声明出处哦~,本篇文章发布于luozhiyun的博客:聊聊 Redis 是如何进行请求处理 - luozhiyun`s Blog
本文使用的Redis 5.0源码
感觉这部分的代码还是挺有意思的,我尽量用比较通俗的方式进行讲解
我记得我在 一文说透 Go 语言 HTTP 标准库 这篇文章里面解析了对于 Go 来说是如何创建一个 Server 端程序的:
用代码表示就是这样:
func (srv *Server) Serve(l net.Listener) error { ...baseCtx := context.Background() ctx := context.WithValue(baseCtx, ServerContextKey, srv)for {// 接收 listener 过来的网络连接rw, err := l.Accept()... tempDelay = 0c := srv.newConn(rw)c.setState(c.rwc, StateNew) // 创建协程处理连接go c.serve(connCtx)}
}
对于 Redis 来说就有些不太一样,因为它是单线程的,无法使用多线程处理连接,所以 Redis 选择使用基于 Reactor 模式的事件驱动程序来实现事件的并发处理。
在 Redis 中所谓 Reactor 模式就是通过 epoll 来监听多个 fd,每当这些 fd 有响应的时候会以事件的形式通知 epoll 进行回调,每一个事件都有一个对应的事件处理器。
如: accept 对应 acceptTCPHandler 事件处理器、read & write 对应readQueryFromClient 事件处理器等,然后通过事件的循环派发的形式将事件分配给事件处理器进行处理。
所以说上面的这个 Reactor 模式都是通过 epoll 来实现的,对于 epoll 来说主要有这三个方法:
//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_create(int size);/** 可以理解为,增删改 fd 需要监听的事件* epfd 是 epoll_create() 创建的句柄。* op 表示 增删改* epoll_event 表示需要监听的事件,Redis 只用到了可读,可写,错误,挂断 四个状态*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);/** 可以理解为查询符合条件的事件* epfd 是 epoll_create() 创建的句柄。* epoll_event 用来存放从内核得到事件的集合* maxevents 获取的最大事件数* timeout 等待超时时间*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
所以我们可以根据这三个方法实现一个简单的 server:
// 创建监听
int listenfd = ::socket();// 绑定ip和端口
int r = ::bind();
// 创建 epoll 实例
int epollfd = epoll_create(xxx);
// 添加epoll要监听的事件类型
int r = epoll_ctl(..., listenfd, ...);struct epoll_event* alive_events = static_cast(calloc(kMaxEvents, sizeof(epoll_event)));while (true) {// 等待事件int num = epoll_wait(epollfd, alive_events, kMaxEvents, kEpollWaitTime);// 遍历事件,并进行事件处理for (int i = 0; i < num; ++i) {int fd = alive_events[i].data.fd;// 获取事件int events = alive_events[i].events;// 进行事件的分发if ( (events & EPOLLERR) || (events & EPOLLHUP) ) {...} else if (events & EPOLLRDHUP) {...} ...}
}
所以根据上面的介绍,可以知道对于 Redis 来说一个事件循环无非也就这么几步:
上面的整个事件循环的过程实际上代码步骤已经写的非常清晰,网上也有很多文章介绍,我就不多讲了。
下面我们讲点网上很多文章都没提及的,看看 Redis 是如何执行命令,然后存入缓存,以及将数据从缓存写回 Client 这个过程。
在前一节我们也提到了,如果有网络事件过来的时候会调用到 readQueryFromClient 函数,它是真正执行命令的地方。我们也就顺着这个方法一直往下看:
server.commands
表中根据命令查找对应的执行函数,然后经过一系列的校验之后,调用相应的函数执行命令,调用 addReply 将要返回的数据写入客户端输出缓冲区;server.commands
会在 populateCommandTable 函数中将所有的 Redis 命令注册进去,作为一个根据命令名获取命令函数的表。
比如说,要执行 get 命令,那么会调用到 getCommand 函数:
void getCommand(client *c) {getGenericCommand(c);
}int getGenericCommand(client *c) {robj *o;// 查找数据if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)return C_OK;...
}robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) {//到db中查找数据robj *o = lookupKeyRead(c->db, key);// 写入到缓存中if (!o) addReply(c,reply);return o;
}
在 getCommand 函数中查找到数据,然后调用 addReply 将要返回的数据写入客户端输出缓冲区。
在上面执行完命令写入到缓冲区后,还需要从缓冲区取出数据返回给 Client。对于数据回写客户端这个流程来说,其实也是在服务端的事件循环中完成的。
这篇文章介绍了整个 Redis 的请求处理模型到底是怎样的。从注册监听 fd 事件到执行命令,到最后将数据回写给客户端都做了个大概的分析。当然这篇文章也和我以往的文章有点不同,没有长篇大论的贴代码,主要我觉得也没啥必要,感兴趣可以顺着流程图去看看代码。
http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf
10 | Redis事件驱动框架(中):Redis实现了Reactor模型吗?-极客时间
Redis 事件机制详解 | 程序员历小冰
https://github.com/Junnplus/blog/issues/37
Redis中单机数据库的实现 - 张浮生 - 博客园