破玩意 | Redis 为什么那么快

我是个 redis 服务,我马上就要启动了

因为我的主人正在控制台输入:

./redis-server

宏观上看下我的流程

突然,主人按下了回车键,不得了了。

shell 程序把我的程序加载到了内存,开始执行我的 main 方法,一切就从这里开始了。

int main(int argc, char **argv) {
   ...
   initServer();
    ...
   aeCreateFileEvent(fd, acceptHandler, ...);
   ...
   aeMain();
   ...
}

不要觉得我这里很复杂,其实主要就三大步。

第一步,我通过 listenToPort() 方法创建了一个 TCP 连接。

破玩意 | Redis 为什么那么快_第1张图片

我的这个方法真是见名知意,而且如果展开看就更会发现没什么神秘的,就是 socket bind listen 标准三步走,建立了一个 TCP 监听,返回了一个文件描述符 fd。

第二步,我通过 aeCreateFileEvent() 方法,将上面那个创建了 TCP 连接返回的文件描述符 fd,加入到一个叫 aeFileEvent的链表中。

破玩意 | Redis 为什么那么快_第2张图片

同时将这个文件描述符绑定一个函数 acceptHandler,这样当有客户端连接进来时,便会执行这个函数。

破玩意 | Redis 为什么那么快_第3张图片

第三步,我通过 aeMain() 方法,将上面的 aeFileEvent 链表中的文件描述符,统统作为 select 的入参,这是 IO 多路复用模式,如果不太了解的同学请阅读,《你管这破玩意叫 IO 多路复用?》。

破玩意 | Redis 为什么那么快_第4张图片

好了,其实就是开启了一个 TCP 监听,然后如果有客户端进来的话,让他执行 acceptHandler 函数而已。

之后我就一直死等着客户端连接了。

void aeMain(aeEventLoop *eventLoop)
{
    eventLoop->stop = 0;
    while (!eventLoop->stop)
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}

破玩意 | Redis 为什么那么快_第5张图片 

展开体验下我的具体工作

此时,另外一个人启动了一个 redis-client,连接到了我。

redis-cli -h host -p port

那么我头上的 fd 就会感知有数据读入,并执行 acceptHandler 方法。

static void acceptHandler(...) {
   ...
    cfd = anetAccept(...);
   ...
    c = createClient(cfd))
   ...
}

可以看到,当有新客户端连接进来时,便会调用 createClient 创建一个专属的 client 为其服务。

static redisClient *createClient(int fd) {
   ...
   aeCreateFileEvent(c->fd, readQueryFromClient, ...);
   ...
}

这里又可以看到,所谓的专属服务,其实仍然是这个 aeCreateFileEvent 函数。

这个上面说了,这个函数的功能就是把文件描述符挂在链表上,然后分配一个处理函数。

当然,这回的处理函数不再是处理新客户端连接的 acceptHandler,而是处理具体客户端传来的 redis 命令的函数 readQueryFromClient

破玩意 | Redis 为什么那么快_第6张图片

不难想象,如果再来一个客户端,又来一个客户端... 那么不断将新客户端的文件描述符挂上去即可,而监听新客户端连接的,始终是最上面那个文件描述符。

破玩意 | Redis 为什么那么快_第7张图片

好了,服务端开启了监听,客户端也连上了服务端,此时我仍然在死等状态,只不过等的不只是新客户端连接到达,还在等待已经连接上的客户端发来命令。

破玩意 | Redis 为什么那么快_第8张图片

请注意,这里的死等,只有一个线程,循环调用 aeProcessEvents 函数,用 select 的方式监听多个文件描述符。放上刚刚 main 方法的第三步,帮大家回忆一下。

void aeMain(aeEventLoop *eventLoop)
{
    eventLoop->stop = 0;
    while (!eventLoop->stop)
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}

当有新客户端建立连接时,会触发 acceptHandler 函数执行,多出一个等待数据的描述符。

当有客户端数据传来时,会触发 readQueryFromClient 函数执行,完成这个命令的操作。

注意,由于只有一个线程在监听这些描述符,并做处理。所以即使客户端并发地发送命令,后面仍然是依次取出命令,顺序执行

这也就是我们常常说的,redis 是单线程的,命令与命令之间是顺序执行,无需考虑线程安全的问题。

为了方便大家吹牛,我来拔高一下

大家发现没,我的启动过程,其实就分成两个大的部分。

一个是监听客户端的请求,就是用 IO 多路复用的方式,监听多个文件描述符,就刚刚那个 aeMain() 方法干的事嘛。

一个是执行相应的函数去处理这个请求,具体执行什么函数就是出现多次的 aeCreateFileEvent() 方法去绑定的,这个相应的函数说得高大上一点,叫做事件处理器

破玩意 | Redis 为什么那么快_第9张图片

这里所谓的连接应答处理器,就是刚刚监听连接的文件描述符所绑定的函数 acceptHandler。

所谓的命令请求处理器,就是监听客户端命令(读事件)的文件描述符绑定的函数 readQueryFromClient。

所谓的命令回复处理器,就是后面要提到的,监听客户端响应(写事件)的文件描述符绑定的函数 sendReplyToClient。

这种一个负责响应 IO 事件,一个负责交给相应的事件处理器去处理,就叫做 Reactor 模式

Redis 正是基于 Reactor 模式开发了自己的文件事件处理器,实现了高性能的网络通信模型,并且保持了 Redis 内部单线程设计的简单性

有点担心这句话吹牛的逼格不够,其实我是参考了《Redis 设计与实现》,截图给大家。

破玩意 | Redis 为什么那么快_第10张图片

具体怎么执行一个 Redis 命令

现在,我们通过一个已建立好连接的客户端,发一个 redis 命令。

 set dibingfa niubi

此时 readQueryFromClient 函数将被执行。

这个函数会去一张表中寻找命令所对应的函数,这部分用的编码技巧叫命令模式。

static struct redisCommand cmdTable[] = {
    {"get",getCommand,2,REDIS_CMD_INLINE},
    {"set",setCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM},
    {"setnx",setnxCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM},
    {"del",delCommand,-2,REDIS_CMD_INLINE},
    {"exists",existsCommand,2,REDIS_CMD_INLINE},
   ...
}

找到了 set 命令对应的函数就是 setCommand

这个函数,最终就会一步步地将 key 和 value 分别存储起来,这部分的源码细节,可以阅读我之前的文章,《Redis 数据结构之字符串的那些骚操作》。

而关于 redis 数据结构与底层对应的编码结构,可以阅读我之前的文章,《面试官问我 redis 的数据类型,我回答了 8 种》。

处理完命令后,就要发送响应给客户端了。

static void setCommand(redisClient *c) {
   ...
    addReply(c, nx ? shared.cone : shared.ok);
}

这个响应,并不是直接同步写回去,当然也不是开启一个线程去异步写回去。

它仍然是调用那个万恶的 aeCreateFileEvent 函数,将 sendReplyToClient 函数挂在需要响应的客户端连接的文件描述符上。

static void addReply(redisClient *c, robj *obj) {
   ...
    aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
    sendReplyToClient, c, NULL) == AE_ERR);
}

好了,这回上一小节挖的坑,终于补上了。

以上这些个破玩意,就是我的启动过程啦,我是不是很可爱。

破玩意 | Redis 为什么那么快_第11张图片

后记

整篇文章我好像没讲 Redis 为啥那么快,因为我感觉这个问题问得不好。

你可以从接收网络请求的 IO 多路复用角度说起,也可以从事件处理器驱动的 Reactor 模式说起,还可以从具体处理命令时的数据结构说起,比如单单是字符串背后的 sds 其实就做了很多的巧妙设计。

如果我是面试官,我会具体让面试者聊聊 Redis 的启动流程,或者 Redis 处理命令的整个流程。

这里面可挖的点挺多的,如果能谈笑风生,那自然是技术水平还不错。

另外,你会发现本文出现的很多唬人的术语,比如 Reactor 模式,事件处理器等,看一遍 Redis 源码后你会发现真的非常简单。

毫不客气地说,一切丝毫不谈具体实现,和你堆砌一大堆唬人名词的文章或者人,都是在耍流氓。

本文我参考的是 Redis3.0.0 源码,但成文时用的讲解代码是 Redis1.0.0,整个网络模块的设计是完全一样的。

你可能感兴趣的:(网络,java,学习,程序人生)