[源码阅读]-Redis核心事件流程
Redis核心流程

1.Redis核心流程中的重要数据结构
struct redisServer {
int port;
int fd;
redisDb *db;
aeEventLoop *el;
list *clients;
...
};
typedef struct redisClient {
int fd;
redisDb *db;
sds querybuf;
list *reply;
} redisClient;
typedef struct redisDb {
dict *dict;
dict *expires;
int id;
} redisDb;
typedef struct redisObject {
void *ptr;
int type;
int refcount;
} robj;
typedef struct aeEventLoop {
long long timeEventNextId;
aeFileEvent *fileEventHead;
aeTimeEvent *timeEventHead;
int stop;
} aeEventLoop;
基于以上数据结构,Redis就可以构建核心的Server与client的交互流程
2.Redis核心流程解析
不像一些科学计算的程序,运行一次就可以产出所有结果。Redis功能是通过不断地对外界事件进行响应实现的。这种类型的程序,一般都存在事件循环线程,不断地响应,并处理外界事件。
int main(int argc, char **argv) {
initServerConfig();
...
initServer();
if (server.daemonize) daemonize();
redisLog(REDIS_NOTICE,"Server started, Redis version " REDIS_VERSION);
...
if (aeCreateFileEvent(server.el, server.fd, AE_READABLE,
acceptHandler, NULL, NULL) == AE_ERR) oom("creating file event");
redisLog(REDIS_NOTICE,"The server is now ready to accept connections on port %d", server.port);
aeMain(server.el);
aeDeleteEventLoop(server.el);
return 0;
}
// ...表示略去的不重要的代码
在main函数入口中,主要做了三件事:
- 初始化服务器配置,并初始化服务器
- 使用aeCreateFileEvent函数在服务器结构的事件循环el中注册了事件,事件的回调函数为acceptHandler。这个函数会在server.fd就绪的时候被调用,进行accept行为,建立对应的redisClient资源。(具体细节会在后面进行介绍)
- 开启aeMain()事件循环,处理事件。在redis-1.0的版本中,使用select来对网络I/O行为进行多路复用,当有就绪事件时,就调用事先在事件中注册的回调函数进行处理。
我们来看看acceptHandler函数中的逻辑:
static void acceptHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
...
cfd = anetAccept(server.neterr, fd, cip, &cport);
...
if ((c = createClient(cfd)) == NULL) {
...
}
...
server.stat_numconnections++;
}
acceptHandler函数主要为客户端连接建立了一个redisClient对象,用来管理后续的请求与响应。那么createClient是如何创建redisClient的呢?
下面是createClient函数的源码:
static redisClient *createClient(int fd) {
redisClient *c = zmalloc(sizeof(*c));
anetNonBlock(NULL,fd);
anetTcpNoDelay(NULL,fd);
if (!c) return NULL;
selectDb(c,0);
c->fd = fd;
c->querybuf = sdsempty();
c->argc = 0;
c->argv = NULL;
c->bulklen = -1;
c->sentlen = 0;
c->flags = 0;
c->lastinteraction = time(NULL);
c->authenticated = 0;
c->replstate = REDIS_REPL_NONE;
if ((c->reply = listCreate()) == NULL) oom("listCreate");
listSetFreeMethod(c->reply,decrRefCount);
listSetDupMethod(c->reply,dupClientReplyValue);
if (aeCreateFileEvent(server.el, c->fd, AE_READABLE,
readQueryFromClient, c, NULL) == AE_ERR) {
freeClient(c);
return NULL;
}
if (!listAddNodeTail(server.clients,c)) oom("listAddNodeTail");
return c;
}
在createClient中,首先会设置网络的I/O类型,以及一些TCP参数(说明连接是以TCP的方式进行的)。而后,会选择与客户端相连的数据库,并对redisClient中的一些属性进行初始化。最后会使用redisClient的句柄fd创建一个新的ae可读事件,在这个事件中,使用readQueryFromClient作为回调函数。最后,新建的这个redisClient会被添加到redisServer的clients链表中。
readQueryFromClient做了什么事呢?
static void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
redisClient *c = (redisClient*) privdata;
char buf[REDIS_IOBUF_LEN];
int nread;
REDIS_NOTUSED(el);
REDIS_NOTUSED(mask);
nread = read(fd, buf, REDIS_IOBUF_LEN);
...
again:
if (c->bulklen == -1) {
/* Read the first line of the query */
char *p = strchr(c->querybuf,'\n');
size_t querylen;
if (p) {
...
if (c->argc && processCommand(c) && sdslen(c->querybuf)) goto again;
return;
} else if (sdslen(c->querybuf) >= REDIS_REQUEST_MAX_SIZE) {
redisLog(REDIS_DEBUG, "Client protocol error");
freeClient(c);
return;
}
}
...
}
在readQueryFromClient函数中,会执行来自客户端buffer中的命令。并且,该事件的生命周期与响应的redisClient生命周期是一致的(被freeClient函数所关闭),当遇到错误或客户端请求关闭时,才会注销该事件。其余情况,会在每次调用之后,仍存在于事件循环el中。我们可以看到,真正用来执行命令的是processCommand函数
在processCommand函数中,有以下逻辑:
static int processCommand(redisClient *c) {
struct redisCommand *cmd;
...
cmd = lookupCommand(c->argv[0]->ptr);
...
/* Exec the command */
dirty = server.dirty;
cmd->proc(c);
if (server.dirty-dirty != 0 && listLength(server.slaves))
replicationFeedSlaves(server.slaves,cmd,c->db->id,c->argv,c->argc);
if (listLength(server.monitors))
replicationFeedSlaves(server.monitors,cmd,c->db->id,c->argv,c->argc);
server.stat_numcommands++;
/* Prepare the client for the next command */
if (c->flags & REDIS_CLOSE) {
freeClient(c);
return 0;
}
resetClient(c);
return 1;
}
这一部分包含一个查找命令的逻辑以及执行命令的逻辑。所有的命令都被放在cmdTable[]中。
我们以get命令为例,看看get命令的proc函数getCommand都做了什么事情:
static void getCommand(redisClient *c) {
robj *o = lookupKeyRead(c->db,c->argv[1]);
if (o == NULL) {
addReply(c,shared.nullbulk);
} else {
if (o->type != REDIS_STRING) {
addReply(c,shared.wrongtypeerr);
} else {
addReplySds(c,sdscatprintf(sdsempty(),"$%d\r\n",(int)sdslen(o->ptr)));
addReply(c,o);
addReply(c,shared.crlf);
}
}
}
先从db中查找对应的redisObject,并且通过addReply方法加入到响应中。那么addReply做了什么事情呢?
static void addReply(redisClient *c, robj *obj) {
if (listLength(c->reply) == 0 &&
(c->replstate == REDIS_REPL_NONE ||
c->replstate == REDIS_REPL_ONLINE) &&
aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
sendReplyToClient, c, NULL) == AE_ERR) return;
if (!listAddNodeTail(c->reply,obj)) oom("listAddNodeTail");
incrRefCount(obj);
}
可以看到,在addReply中生成了一个ae可写事件,并注册了名为sendReplyToClient的回调函数。
sendReplyToClient是redisServer解析query,并响应query结果链路中的最后一环:
static void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {
redisClient *c = privdata;
int nwritten = 0, totwritten = 0, objlen;
robj *o;
REDIS_NOTUSED(el);
REDIS_NOTUSED(mask);
if (server.glueoutputbuf && listLength(c->reply) > 1)
glueReplyBuffersIfNeeded(c);
while(listLength(c->reply)) {
...
nwritten = write(fd, ((char*)o->ptr)+c->sentlen, objlen - c->sentlen);
...
}
if (nwritten == -1) {
if (errno == EAGAIN) {
nwritten = 0;
} else {
redisLog(REDIS_DEBUG,
"Error writing to client: %s", strerror(errno));
freeClient(c);
return;
}
}
if (totwritten > 0) c->lastinteraction = time(NULL);
if (listLength(c->reply) == 0) {
c->sentlen = 0;
aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
}
}
这个函数负责不停的将redisClient.reply中的响应写到客户端。直到没有reply可写之后,调用aeDeleteFileEvent来删除这个事件。(这个事件的生命周期随着一次调用响应结束而结束)。3
3. 总结
看到这里,大家可以发现,redis的核心ae循环处理流程中,始终只有一个主线程在运行。通过事件驱动的方式,处理相应的事件。同时,使用select多路复用来快速响应I/O事件,达到了很高的效率。
参考
- redis-1.0 SOURCE-CODE
- <Go手写Redis>系列
[源码阅读]-Redis核心事件流程的更多相关文章
- SpringMVC源码阅读:核心分发器DispatcherServlet
1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将介绍SpringMVC的核 ...
- Struts2源码阅读(一)_Struts2框架流程概述
1. Struts2架构图 当外部的httpservletrequest到来时 ,初始到了servlet容器(所以虽然Servlet和Action是解耦合的,但是Action依旧能够通过httpse ...
- Linux 0.11源码阅读笔记-文件IO流程
文件IO流程 用户进程read.write在高速缓冲块上读写数据,高速缓冲块和块设备交换数据. 什么时机将磁盘块数据读到缓冲块? 什么时机将缓冲块数据刷到磁盘块? 函数调用关系 read/write( ...
- SpringMVC源码阅读:Controller中参数解析
1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...
- SpringMVC源码阅读系列汇总
1.前言 1.1 导入 SpringMVC是基于Servlet和Spring框架设计的Web框架,做JavaWeb的同学应该都知道 本文基于Spring4.3.7源码分析,(不要被图片欺骗了,手动滑稽 ...
- SpringMVC源码阅读:拦截器
1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...
- SpringMVC源码阅读:定位Controller
1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码分析,弄清楚Spr ...
- Redis源码阅读(一)事件机制
Redis源码阅读(一)事件机制 Redis作为一款NoSQL非关系内存数据库,具有很高的读写性能,且原生支持的数据类型丰富,被广泛的作为缓存.分布式数据库.消息队列等应用.此外Redis还有许多高可 ...
- Caddy源码阅读(二)启动流程与 Event 事件通知
Caddy源码阅读(二)启动流程与 Event 事件通知 Preface Caddy 是 Go 语言构建的轻量配置化服务器.https://github.com/caddyserver/caddy C ...
- CI框架源码阅读笔记1 - 环境准备、基本术语和框架流程
最开始使用CI框架的时候,就打算写一个CI源码阅读的笔记系列,可惜虎头蛇尾,一直没有行动.最近项目少,总算是有了一些时间去写一些东西.于是准备将之前的一些笔记和经验记录下来,一方面权作备忘,另一方面时 ...
随机推荐
- 在vue中引入外部的css文件
在vue中引入外部的css文件在项目的src文件下,新建一个style文件夹,存放css文件. 1. 全局引入将外部的css文件放到style文件下,引入外部文件只需在main.js文件中 impor ...
- vue,js直接导出excel,xlsx的方法,XLSX_STYLE 行高设置失效的问题解决,冻结窗体修改支持
1.先安装依赖:xlsx.xlsx-style.file-saver三个包 npm install xlsx xlsx-style file-saver 出现错误: This relative mod ...
- three.js+vue智慧社区web3d数字孪生三维地图
案例效果截图如下: 具体案例场景和功能,详见b站视频: https://www.bilibili.com/video/BV1Bb421E7WL/?vd_source=7d4ec9c9275b9c7d1 ...
- “应用程序无法正常启动(0xc000007)”处理办法
前几天使用非静态方式编译了一个程序,在部分系统上运行提示缺少msvcp140.dll,就从VS2019安装目录找了一个同名文件放在了程序同级目录,程序也可以正常运行了.今天重新打开虚拟机,突然就报了这 ...
- MFC应用当中,修改tab顺序
打开资源文件,按"Ctrl+D"组合键,可以查看控件的Tab顺序,使用鼠标顺序点击即可改变Tab顺序,此方法在Visual Studio 2022版本有效,其余版本没有测试过.
- 用“tar | split ”将文件分包压缩
1.一次打包: tar cjf - directory/ | split -b 100m -d -a 1 - filename.tar.bz2. (directory/):待压缩的目录名. (spli ...
- (系列十一)Vue3框架中路由守卫及请求拦截(实现前后端交互)
说明 该文章是属于OverallAuth2.0系列文章,每周更新一篇该系列文章(从0到1完成系统开发). 该系统文章,我会尽量说的非常详细,做到不管新手.老手都能看懂. 说明:OverallAuth2 ...
- gal game 杂谈——前言
gal game 杂谈--前言 大年三十凌晨(早上)打算开始写了吧,作为第一篇先写一些前言好了. 第一次接触gal game还是在B站上看到有人玩<我和她的世界末日>当时觉得挺有意思的,加 ...
- Java反编译器剖析
本文由 ImportNew - 邬柏 翻译自 javacodegeeks.如需转载本文,请先参见文章末尾处的转载要求. Importnew注:如果你也对Java技术翻译分享感兴趣,欢迎加入我们的Jav ...
- 在vue中使用XLSX导出表格
安装依赖 npm install file-saver xlsx -S 然后在需要的页面中引入依赖包 import FileSaver from 'file-saver'; import XLSX f ...