redis源码笔记(一) —— 从redis的启动到command的分发

时间:2023-03-08 16:17:27
redis源码笔记(一) —— 从redis的启动到command的分发

本作品采用知识共享署名 4.0 国际许可协议进行许可。转载联系作者并保留声明头部与原文链接https://luzeshu.com/blog/redis1

本博客同步在http://www.cnblogs.com/papertree/p/7159802.html


这个系列博客大部分完成于一年前,基于3.0.5版本(但是代码行数不一定完全相符,调试过程中会修改一些代码)。

这一篇博客针对第二篇涉及到的redisClient、redisDb、redisObject(robj)等几个结构体,以及redis程序的启动到循环、到分发command来进行讲解。

看redis源码时有个很深的感触就是c语言虽然不是“面向对象编程语言”,原生不支持类、继承等面向对象编程语言的概念,但不影响在c语言上运用“面向对象编程思想”进行开发。比如很多模块会定义一个结构体,还有相关的一系列函数,这些函数使用该结构体的指针类型作为第一个参数,实际上这是模拟了this指针的做法。可以把这些函数和结构体看成一个类的方法和成员。

1.1 redis的启动到进入等待 / aeEventLoop结构体

我们知道redis也是一个普通的服务端程序,监听6379(默认)端口。从main函数启动,最终进入事件驱动库进行循环等待。比如node的libuv。那么redis自己实现了一个简单的事件驱动库,放在ae.c文件。并且对系统层面支持的IO复用接口进行了封装,比如epoll(linux)、kqueue(OS X、FreeBSD等)、evport(Solaris 10等)、select。来看下图,可以知道当系统不支持其他IO复用接口时,默认使用了select模型。

redis源码笔记(一) —— 从redis的启动到command的分发

图1-1-1

看到这段代码,产生一个疑问,我们都知道windows下最高效的IO复用模型应该属IOCP了,比如node使用的libuv库,就对IOCP进行了封装。但ae.c里面看到,redis不支持IOCP?于是针对这个问题,笔者google了一下,发现了两个有意思的链接。

http://www.oschina.net/news/23944/redis-deny-microsoft-windows-fixpack

http://oldblog.antirez.com/post/redis-win32-msft-patch.html

大体情况就是redis原生不支持IOCP,于是微软采用libuv把redis移植到了windows,在github上给redis提交了补丁。但redis作者拒绝将此补丁加入主干代码。第二个链接是redis作者对此的解释。

回归正题,那么可以看到redis从启动到进入等待的过程并不复杂。redis.c/main() -> ae.c/aeMain() -> (ae_epoll.c、ae_kqueue.c、ae_select.c等)/aeApiPoll(),看下图:

redis源码笔记(一) —— 从redis的启动到command的分发

图1-1-2

1.1.1 redisServer结构体

看一下redisServer的结构体定义:

redis源码笔记(一) —— 从redis的启动到command的分发

图1-1-3

注意几个成员,与稍后讲解有关:

aeEventLoop *el:这里表示的就是一个事件驱动库的结构体

int ipfd[REDIS_BINDADDR_MAX]:redis服务监听的socket fd

int ipfd_count:ipfd的计数成员

redis有一个全局的变量struct redisServer server,保存了当前redisServer的各种信息,包括aeEventLoop类型的server.el成员等。

当main函数调用了ae事件驱动库的aeMain()时,传了server.el,这里就是前面说的面向对象编程思想的做法了,server.el充当一个this指针。

当进入ae.c模块时,我们来看看aeEventLoop结构体。

1.1.2 aeEventLoop结构体

看一下 aeEventLoop结构体和几个相关的结构体、函数指针类型的定义:

redis源码笔记(一) —— 从redis的启动到command的分发

图1-1-4

来看几个关键成员及相关的结构体,以及与之相关的方法:

1.1.2.1 void *apidata与aeApiState结构体与aeApiCreate()

从下图1-1-5里的aeApiCreate()函数里面可以看到,apidata实际上放的是一个aeApiState结构体指针,可以看到ae_epoll.c(图1-1-5左)、ae_vport.c(图1-1-5右)分别对aeApiState有不同的结构体定义,实际上是对不同操作系统(不同复用接口)的封装。

按照上面说的“面向对象编程思想”,aeApiState结构体相关的方法的“this指针”应该是aeApiState指针。

可以从图1-1-5中看到aeApiCreate()、aeApiResize()等几个跟aeApiState结构体相关的方法的定义,发现他们的“this指针”都是aeEventLoop*类型,而不是aeApiState*,当方法内部访问aeApiState时,通过eventLoop->apidata去访问。

注意到这几个方法内部(比如aeApiAddEvent),并不都仅仅只是使用了eventLoop->apidata,同时也访问了eventLoop的其他成员,所以这里使用aeEventLoop*作为“this指针”是合理的。

redis源码笔记(一) —— 从redis的启动到command的分发

图1-1-5

1.1.2.2 events成员(aeFileEvent结构体的动态数组,以fd为索引)与aeCreateFileEvent()

aeCreateFileEvent()是aeEventLoop的一个方法成员,通过该方法,往aeEventLoop的events里添加一个aeFileEvent对象,可以看到图1-1-4的定义。可以看出aeFileEvent实际上代表的是一个事件handler,封装了事件的回调函数,以及对应的clientData。当epoll_wait()监听的fd有事件到来时,该对象被取出,回调函数被执行,clientData被回传。图1-1-4中的两个函数指针定义,就是该回调函数的类型。

这里举两个关键的使用位置:

1. 监听socket的回调函数

在main函数开始后,initServer的时候,会调用aeCreateFileEvent(),把server.ipfd[]中监听的fd依次创建一个aeFileEvent对象,响应函数为(aeFileProc*) acceptTcpHandler,加进事件驱动库,并添加到 server.el->events 成员里面,以fd为数组索引下标。注意了此时的clientData是NULL的,看一下此处的代码:

redis源码笔记(一) —— 从redis的启动到command的分发

图1-1-6

2. 连接socket的回调函数

当有连接到来的时候,acceptTcpHandler被触发,此时redis创建了一个redisClient的对象,并同样调用了aeCreateFileEvent(),把相应的回调函数(aeFileProc*) readQueryFromClient同样封装成aeFileEvent对象,加进事件驱动库,添加到server.el->events成员里面,以fd为索引下标,此时的clientData是对应的redisClient对象,这个redisClient标识了一个客户端的连接,redisClient结构体、以及readQeuryFromClient如何分发处理command的详细介绍在1.2.3节。

来看一下对应的调用代码:

redis源码笔记(一) —— 从redis的启动到command的分发

图1-1-7

可以看到networking.c文件里面,acceptTcpHandler、readQueryFromClient都是aeFileProc类型的回调函数。

*1.1.2.3 aeCreateTimeEvent()方法与timeEventHead成员(aeTimeEvent结构体的链表头,所以aeTimeEvent存在next成员)

这里额外讲多一个结构体类型,不在本篇博客“从启动到进入等待、从接收连接到分发命令”的主线,但是在第三篇博客《redis源码笔记(三) —— redis的哨兵模式以及高可用性》的3.2节里面会用到。

我们知道server进入epoll_wait()之后会进入等待,但是事实上redis-server是不断被定时唤醒的,因为它后台有一个定时任务函数 —— serverCron。这个后台执行任务被封装在aeTimeEvent对象里面,aeEventLoop对象(server.el)通过自身的aeCreateTimeEvent()方法去往自身的timeEventHead链表添加这样一个对象。在图1-1-6中可以看到initServer里面有aeCreateTimeEvent这么一个过程。

这里需要讲的是:这个后台任务是如何被周期性执行的,还有执行周期是什么。

看到图1-1-8中aeCreateTimeEvent的定义,看到第二个参数milliseconds,再看到图1-1-6里面initServer添加serverCron时该参数为1,不要误以为这个后台任务就是执行周期为1ms。

redis源码笔记(一) —— 从redis的启动到command的分发

图1-1-8

先看到aeProcessEvents,每次进入aeApiPoll()前,aeProcessEvents都会调用aeSearchNearestTimer从eventLoop->timeEventHead 去找到第一个aeTimeEvent对象,通过该对象的when_sec和when_ms去计算下一次监听中断的时长。

redis源码笔记(一) —— 从redis的启动到command的分发

图1-1-9

那么当上面根据eventLoop->timeEventHead计算的最短时长到达后,aeApiPoll返回,执行processTimeEvents,对eventLoop->timeEventHead里面所有过时了的aeTimeEvent对象进行“执行回调”,看代码:

redis源码笔记(一) —— 从redis的启动到command的分发

图1-1-10

那么可以看到这个回调函数,大多数情况下就是上面的后台任务函数serverCron。根据该函数返回的retval,加上当前时间并更新到当前的aeTimeEvent对象的时间成员上面,那么就是说,这个后台任务的执行周期,是由该后台任务的返回值决定的,如果该函数返回了AE_NOMORE,那么这个aeTimeEvent对象就会从eventLoop->timeEventHead链表里面删除。

来看看serverCron函数的返回值:

redis源码笔记(一) —— 从redis的启动到command的分发

图1-1-11

可以看出serverCron返回的是一个变量,1000/server.hz,这个hz就是频率的意思(还记得物理里面的单位吗,时间的倒数就是频率),比如频率为10,那么1000ms里面10次的间隔就是100ms。这个server.hz 可以通过配置文件redis.conf里面的hz 选项进行设置。

另外注意到上面的run_with_period这个宏定义。这个比如run_with_period(100) {} 限制了该代码块的“最小周期”是100ms,比如说,你的server.hz 是2,那么你的serverCron周期是500ms,那么周期大于100ms可以接受,每次执行serverCron时run_with_period(100)的代码块都会被执行。如果server.hz 是20,那么serverCron周期是50ms,那么周期小于100ms了,run_with_period(100){}的代码块会根据server.cronloops的计数来判断,每两次serverCron执行一次,如果server.hz是100,serverCron周期是10ms,那么每10次serverCron执行一次代码块,保证run_with_period(100)里面的代码真的是每100ms执行一次。

那么回到上面aeCreateFileEvent的第二个参数milliseconds、以及图1-1-6在initServer时调用这个时候传的“1”是指什么呢?其实跟processTimeEvents每次执行serverCron后拿到下一个周期的监听时长、添加到当前的aeTimeEvent对象上一样,这个initServer调用aeCreateFileEvent时传的“1”也表示下一个周期的监听时长,也就是这个aeTimeEvent封装的serverCron第一次被执行应该是在当前时间的1ms之后,而随后的周期性执行才是根据serverCron本身返回的值去决定下一个周期监听时长。

但是这里注意的是,serverCron第一次被执行也往往不是在1ms之后,我们看到图1-1-9的378到385这几行代码。在进入aeApiPoll前会进行计算下一个周期监听时长,计算方式就是从eventLoop->timeEventHead取出最近的那个aeTimeEvent,减去当前时间。但是当initServer执行aeCreateFileEvent()到这几行代码的时候,往往历经了几毫秒,那么这个最近的aeTimeEvent的时间已经过期了几毫秒。那么从上面的计算方式可以发现,tvp表示的应该是{900+毫秒,-1秒},但是第384行代码会把负值的秒清零。所以往往第一次serverCron的调用会是在900+毫秒之后。

小结:

通过上面几个结构体和相关方法的讲解,我们大概知道了从main函数启动,到进入监听等待的过程中,涉及到的相关结构体及方法。

下面来看一下从接收到客户端的连接请求、到command的分发过程。


1.2 从客户端的连接请求到command的分发

从1.1节看到,与“对客户端的连接请求处理”相关的是aeFileEvent结构体,当tcp连接请求到来时,acceptTcpHandler被调用,并针对该tcp连接创建一个新的aeFileEvent对象,用于处理后续到来的command,这个新建的aeFileEvent对象的回调函数是readQueryFromClient。

1.2.1 接收客户端连接请求 / acceptTcpHandler()

上面1.1.2.2节对acceptTcpHandler里面如何创建一个aeFileEvent对象(clientData*为redisClient指针,回调函数为readQueryFromClient)讲的很清楚。

1.2.2 接收客户端的命令 / readQueryFromClient()

上面1.1.2.2节也说了,当epoll_wait()监听的fd有事件到来时,该对象被取出,回调函数被执行,clientData被回传。

当建立的tcp连接有数据到来时,调用回调函数readQueryFromClient(),并把clientData回传(即privdata参数),实际上clientData就是针对该连接的redisClient对象。

可以看到这里,几乎都是对redisClient的操作。这里结合redisClient的结构体及相关的方法,来对这个流程进行讲解。

首先看源码:

1154 void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
1155 redisClient *c = (redisClient*) privdata;
1156 int nread, readlen;
1157 size_t qblen;
1158 REDIS_NOTUSED(el);
1159 REDIS_NOTUSED(mask);
1160
1161 server.current_client = c;
1162 readlen = REDIS_IOBUF_LEN;
1163 /* If this is a multi bulk request, and we are processing a bulk reply
1164 * that is large enough, try to maximize the probability that the query
1165 * buffer contains exactly the SDS string representing the object, even
1166 * at the risk of requiring more read(2) calls. This way the function
1167 * processMultiBulkBuffer() can avoid copying buffers to create the
1168 * Redis Object representing the argument. */
1169 if (c->reqtype == REDIS_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
1170 && c->bulklen >= REDIS_MBULK_BIG_ARG)
1171 {
1172 int remaining = (unsigned)(c->bulklen+2)-sdslen(c->querybuf);
1173
1174 if (remaining < readlen) readlen = remaining;
1175 }
1176
1177 qblen = sdslen(c->querybuf);
1178 if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
1179 c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
1180 nread = read(fd, c->querybuf+qblen, readlen);
1181 if (nread == -1) {
1182 if (errno == EAGAIN) {
1183 nread = 0;
1184 } else {
1185 redisLog(REDIS_VERBOSE, "Reading from client: %s",strerror(errno));
1186 freeClient(c);
1187 return;
1188 }
1189 } else if (nread == 0) {
1190 redisLog(REDIS_VERBOSE, "Client closed connection");
1191 freeClient(c);
1192 return;
1193 }
1194 if (nread) {
1195 sdsIncrLen(c->querybuf,nread);
1196 c->lastinteraction = server.unixtime;
1197 if (c->flags & REDIS_MASTER) c->reploff += nread;
1198 server.stat_net_input_bytes += nread;
1199 } else {
1200 server.current_client = NULL;
1201 return;
1202 }
1203 if (sdslen(c->querybuf) > server.client_max_querybuf_len) {
1204 sds ci = catClientInfoString(sdsempty(),c), bytes = sdsempty();
1205
1206 bytes = sdscatrepr(bytes,c->querybuf,64);
1207 redisLog(REDIS_WARNING,"Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)", ci, bytes);
1208 sdsfree(ci);
1209 sdsfree(bytes);
1210 freeClient(c);
1211 return;
1212 }
1213 processInputBuffer(c);
1214 server.current_client = NULL;
1215 }

第1180代码读取当前TCP连接收到的数据,这些数据正是redis命令行的TCP数据格式。

在一个窗口“gdb src/redis-server”,并且“break networking.c:1180”,然后“run”。

tmux开另一个pane,运行“src/redis-cli”,然后输入“keys *”命令。

此时gdb会在断点的地方停下,然后“next”,查看 redisClient的querybuf成员。

gdb$ p c->querybuf
$8 = (sds) 0x7ffff0121008 "*2\r\n$4\r\nkeys\r\n$1\r\n*\r\n"

这便是redis-server接收到客户端的命令的最原始的数据(当然还有更原始的mac层、ip层的数据包是由系统处理的)。

关于redis命令交互的协议,文档上有详细介绍: https://redis.io/topics/protocol

最后readQeuryFromClient()->processInputBuffer(c)->processCommand() 进行command的分发和处理。

processCommand() 在src/redis.c 里面。同时,该文件里面有一个全局表维护着命令与对应的处理函数:

 123 struct redisCommand redisCommandTable[] = {
124 {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
125 {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
126 {"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
127 {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
128 {"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
129 {"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},
130 {"strlen",strlenCommand,2,"rF",0,NULL,1,1,1,0,0},
131 {"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
132 {"exists",existsCommand,-2,"rF",0,NULL,1,-1,1,0,0},
133 {"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0},
134 {"getbit",getbitCommand,3,"rF",0,NULL,1,1,1,0,0},
135 {"setrange",setrangeCommand,4,"wm",0,NULL,1,1,1,0,0},
136 {"getrange",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
137 {"substr",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
138 {"incr",incrCommand,2,"wmF",0,NULL,1,1,1,0,0},
139 {"decr",decrCommand,2,"wmF",0,NULL,1,1,1,0,0},
140 {"mget",mgetCommand,-2,"r",0,NULL,1,-1,1,0,0},
141 {"rpush",rpushCommand,-3,"wmF",0,NULL,1,1,1,0,0},
142 {"lpush",lpushCommand,-3,"wmF",0,NULL,1,1,1,0,0},
143 {"rpushx",rpushxCommand,3,"wmF",0,NULL,1,1,1,0,0},
144 {"lpushx",lpushxCommand,3,"wmF",0,NULL,1,1,1,0,0},
145 {"linsert",linsertCommand,5,"wm",0,NULL,1,1,1,0,0},
146 {"rpop",rpopCommand,2,"wF",0,NULL,1,1,1,0,0},
147 {"lpop",lpopCommand,2,"wF",0,NULL,1,1,1,0,0},
148 {"brpop",brpopCommand,-3,"ws",0,NULL,1,1,1,0,0},
149 {"brpoplpush",brpoplpushCommand,4,"wms",0,NULL,1,2,1,0,0},
150 {"blpop",blpopCommand,-3,"ws",0,NULL,1,-2,1,0,0},
151 {"llen",llenCommand,2,"rF",0,NULL,1,1,1,0,0},
152 {"lindex",lindexCommand,3,"r",0,NULL,1,1,1,0,0},
153 {"lset",lsetCommand,4,"wm",0,NULL,1,1,1,0,0},
154 {"lrange",lrangeCommand,4,"r",0,NULL,1,1,1,0,0},
155 {"ltrim",ltrimCommand,4,"w",0,NULL,1,1,1,0,0},
156 {"lrem",lremCommand,4,"w",0,NULL,1,1,1,0,0},
157 {"rpoplpush",rpoplpushCommand,3,"wm",0,NULL,1,2,1,0,0},
158 {"sadd",saddCommand,-3,"wmF",0,NULL,1,1,1,0,0},
159 {"srem",sremCommand,-3,"wF",0,NULL,1,1,1,0,0},
160 {"smove",smoveCommand,4,"wF",0,NULL,1,2,1,0,0},
161 {"sismember",sismemberCommand,3,"rF",0,NULL,1,1,1,0,0},
......
287 };