怎么样用Redis构建自己的代码帝国?(Redis的架构和核心原理)

本文介绍了 Redis 核心原理和架构:基于事件驱动的模型。事件模型是构成 Redis 内核的引擎,Redis 的丰富功能和组件都是构建在这个模型上的。如果你使用过 Redis,那么本文可以为你打开一道进入 Redis 内部世界的门,窥探 Redis 如何构建它的帝国。

本文先对 Redis 使用的事件模型和原理进行介绍,然后按以下主题顺序展开:

Redis 主程序启动流程

事件循环(eventloop)

事件处理器 (event handler)

事件处理流程

最后以一次客户端 SET 命令操作为例子,讲解一个请求在 Redis 内部的流转是如何完成的。

阅读之前

为了方便公众号上进行阅读,帮助读者快速掌握 Redis 核心原理,本文对 Redis 模型进行了简化,去掉了大量的检查和异常处理流程,并且仅在必要的时候通过代码说明。

本文参考的源码基于编写时的最新分支 Redis 5.0.3,实际对照中发现 Redis 的核心逻辑在历史版本迭代中变化不大,也体现了 Redis 的这个核心逻辑的地位。

一、Redis 事件驱动模型

1.1 事件驱动模型

事件驱动,顾名思义,只有在发生某些事件的时候,程序才会有所行动。

事件驱动模型在架构设计领域也称为 Reactor 模式,体现的是一种被动响应的特征。

事件驱动模型通常可以抽象为如下图所示流程:

主程序处于一个阻塞状态的事件循环(event loop)中等待事件(event),当有事件发生时,根据事件的属性分发到相应的处理函数进行处理。事件以并发的方式发送到服务处理器 (service handler),服务处理器将事件整合到一个有序队列中(这过程称为 demultiplexes),并分发到具体的请求处理器 (request handler)进行处理。

为了阅读的方便,因为「事件」这个词在中文中较常见,所以下文针对事件模型中的「事件」等专用术语,会进行特定的标识,如:事件循环 (event loop),事件 (event),处理器 (handler)等。

1.2 Redis 核心原理

Redis 在事件驱动模型下工作,当有来自外部或内部的请求的时候,才会执行相关的流程。

Redis 程序的整个运作都是围绕事件循环 (event loop)进行的。

事件循环对于 Redis 而言,就像是一台车的引擎一样,提供了整个系统所需的流转动力。所有其他的组件都是基于这个引擎的基础上组合和构建起来的。可以说理解了 Redis 的事件循环就能了解 Redis 的工作原理的核心。

Redis 事件模型如下图所示:

事件循环 eventloop同时监控多个事件,这里的事件本质上是 Redis 对于连接套接字的抽象。

当套接字变为可读或者可写状态时,就会触发该事件,把就绪的事件放在一个待处理事件的队列中,以有序 (sequentially)、同步 (synchronously) 的方式发送给事件处理器进行处理。这个过程在 Redis 中被称为Fire。

Redis 的事件循环会保存两个列表:events和fired列表,前者表示正在监听的事件,后者表示就绪事件,可以被进一步执行。

在具体实现时,Redis 采用 IO 多路复用 (multiplexing) 的方式,封装了操作系统底层 select/epoll 等函数,实现对多个套接字 (socket) 的监听,这些套接字就是对应多个不同客户端的连接。

最后由对应的处理器将处理的结果返回给客户端去。

Redis事件的来源有两种:文件事件和时间事件,限于篇幅问题,本文主要介绍文件事件的处理流程,时间事件会在文章最后做简要的说明。

以上就概括了Redis 处理用户请求的大致过程。从这个过程我们可以发现:

Redis 处理所有命令都是顺序执行的,其中包括来自客户端的连接请求。所以当 Redis 在处理一个复杂度高、时间很长的请求(比如 KEYS 命令)的时候,其他客户端的连接都没办法相应。

Redis 内部定时执行的任务也是放在顺序队列中处理,其中也可能包含时间较长的任务,比如自动删除一个过期的大 Key,比如很大 list, hash, set 等。所以有时候会遇到明明业务没有主动操作复杂,但也会出现卡顿的问题。

1.3 事件驱动模型的优势

有利于架构解耦和模块化开发

有利于功能架构实现上更加解耦,模块的可重用性更高。因事件循环的流程本身和具体的处理逻辑之间是独立的,只要在创建事件的时候关联特定的处理逻辑(事件处理器),就可以完成一次事件的创建和处理。

有利于减小高并发量情况下对性能的影响

根据论文 SEDA: An Architecture for Well-Conditioned, Scalable Internet Services 的测试结果显示,相比一个连接分配一个线程的模型, Reactor 模式(固定线程数)在连接数增大的情况下吞吐量不会明显降低,延时也不会也受到显著的影响。

二、事件循环的 Redis 实现

下面开始,会对 Redis 如何实现事件循环进行说明,会涉及到一些源码的实现部分,如果不感兴趣可以直接跳到第三节看 Redis 怎么利用事件处理模型来处理具体的命令。

2.1 Redis 事件循环 Event Loop

Redis 的事件循环,最直观的理解,就是一个在不断等待事件的一个无限循环,直到 Redis 程序退出。

Redis 实现事件循环主要涉及三个源码文件:server.c, ae.c, networking.c。

server.c 的 main()函数是整个 Redis 程序的开始,我们也从这里开始观察 Redis 的行为。

ae.c实现事件循环和事件的相关功能。

networking.c则负责处理网络IO相关的功能。

a. 初始化 Redis 配置

初始化的过程主要做三个事情:

加载配置

创建事件循环

执行事件循环

简化后的代码如下:(跳过不影响理解)

// 0. 定义服务器主要结构体, 加载服务器配置

struct redisServer server;

initServerConfig();

loadServerConfig();

// 1. 根据配置参数初始化,

initServer()

{

// 1.1 实际创建事件循环

server.el = aeCreateEventLoop();

// 1.2 为事件循环注册一个可读事件,用于响应外部客户端请求

aeCreateFileEvent(server.el, AE_READABLE, acceptTcpHandler)

}

// 2. 执行事件循环,等待连接和命令请求

aeMain(server.el);

初始化过程中被创建的server.el包含了两个事件的列表,它的结构体实现如下:

typedef struct aeEventLoop

{

aeFileEvent events[AE_SETSIZE]; /* 注册的事件,被 eventloop 监听 */

aeFiredEvent fired[AE_SETSIZE]; /* 有读写操作需要执行的事件(就绪事件) */

} aeEventLoop;

b. 创建事件循环

主循环体aeMain()在ae.c文件中被实现,简化后的代码如下:

void aeMain(aeEventLoop *eventLoop) {

while (!eventLoop->stop) {

aeProcessEvents(eventLoop, AE_ALL_EVENTS);

}

}

事件循环主要就是一个while循环,不断去轮询是否有就绪的事件需要处理,具体的处理函数是aeProcessEvents,接下来会有对这个函数有更详细的介绍。

c. 创建用于监听端口的事件

在上述 Redis 在初始化时,程序会创建一个关联了acceptTcpHandler处理器的可读事件:

aeCreateFileEvent(server.el, AE_READABLE, acceptTcpHandler)

这个可读事件注册到事件循环中,就实现了 Redis 对外提供的服务地址和端口的连接服务。具体的内容下一个小节事件处理器中介绍。

2.2 事件处理器 Event Handler

所有事件被创建时,都会关联一个处理器 (handler),并注册到事件循环中,事件处理器用于具体的读写操作。

Redis 的常用几个事件处理器有:

响应连接的处理器acceptTcpHandler()

读取客户端命令的处理器readQueryFromClient()

返回处理结果的处理器sendReplyToClient()

以上处理器均在networking.c文件下实现,该文件负责 Redis 所有网络 IO 功能的实现。

一个客户端一次正常的连接和命令操作流程,可以通过上述三个处理器完成。

当 Redis 需要监听某个套接字的时候,就会创建一个事件,并注册到事件循环中进行监听,Redis 将处理器以参数的方式关联到事件中。

比如以下是注册一个可读事件的操作:

aeCreateFileEvent(server.el, fd, AE_READABLE, readQueryFromClient, c)

server.el:事件循环 eventloop,一个服务器只有一个el

fd:表示这个客户端连接的文件描述符,每个客户端连接对应一个

AE_READABLE:表示这是一个可读事件,可以理解为客户端准备进行写操作

readQueryFromClient: 这个事件关联的处理器,当事件就绪后,就会调用此处理器

c:表示这个客户端在Redis中指向的变量

注册完毕后,事件循环就会将这个事件(套接字)加入到监听的范围,当事件可读时,Redis 就会将这个事件发送到待处理事件队列中等待处理,等到可读就绪时,会被readQueryFromClient处理器处理。

可以看到整个过程中事件循环和不同处理器之间是解耦的,互不干扰。这样实现提高了代码的简洁和重用。

2.3 事件处理 Process Events

在 Redis 完成初始化、创建事件循环后,就会处于等待和处理事件的状态:无限循环aeProcessEvents()函数。

这个函数在ae.c中实现,该文件主要负责事件循环的实现,在aeProcessEvents()中具体做了几个事情:

调用IO多路复用函数(select, epoll, evport, kqueue中的其中一种),阻塞等待事件变成就绪状态或者直到超时,如果有事件就绪,就会将相应事件加入到eventLoop的待处理事件队列 eventLoop->fired 中,然后进入下一个循环。

numevents = aeApiPoll(eventLoop, tvp);

如果在上一步中,发现有numevents个事件被触发,就会将就绪队列的事件一个个按顺序进行处理,处理的函数为

for (j = 0; j < numevents; j++) {

aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];

fe->rfileProc() // 读事件处理

fe->wfileProc() // 写事件处理

}

fe就是要处理的文件事件 file event,对应读操作或写操作。至于处理的具体操作,则由创建事件时自身关联的处理器决定的,事件循环不需要关注。

最后一步:如果有时间事件,则进行时间事件的处理:

processTimeEvents(eventLoop);

至此,Redis 的事件循环的机制已经介绍完毕,可以观察到整个事件循环的逻辑过程都没有涉及具体的命令操作,只需要定义事件的类型和处理器即可。可以说这部分就是Reactor 模式体现出来的一个好处:接收事件和处理流程的实现相互解耦。

三、一次命令操作的完整流程

本章是建立在 Redis 已经完成了初始化工作,主要是创建事件循环之后,Redis 接受一个客户端操作的完整流程的介绍。如果对初始化过程还有问题,请参考上文。

本章主要分为两个阶段:

第一个阶段:一个外部客户端与 Redis 服务器建立 TCP 连接。

比如我们常用的 Telnet 到 Redis 端口的操作。

➜ ~ telnet 127.0.0.1 6379

Trying 127.0.0.1...

Connected to 127.0.0.1.

Escape character is '^]'.

第二阶段:已经建立连接的客户端,对Redis 发起一次SET命令的操作。

set a 1

+OK

3.1 一个客户端连接进服务器的过程

如图,展示一个新的外部客户端与 Redis 服务器建立连接的过程。

当有客户端连接到 Redis 服务器的时候,注册在事件循环中的监听服务端口的事件就会变成读就绪状态,从而触发这个事件到待处理事件队列中,准备调用acceptTcpHandler进行处理。

为在服务器端创建一个对应本次连接的套接字。

把服务端套接字的文件描述符cfd作为参数,创建client变量。

为该客户端连接创建并注册一个关联了readQueryFromClient处理器的可读事件到事件循环,用于下一步接收并执行命令的工作。

3.2 一次客户端连接和调用命令的执行流程

如图展示一个客户端已经完成了连接,对 Redis 服务器发起一次SET操作后,Redis 处理命令的完整流程。

在上一节中提到,当一个客户端建立连接后,会有一个可读事件关联到事件循环,等待接收命令。当有客户端发起一次命令操作后,Redis 就会调用readQueryFromClient处理器,对用户发送过来的请求,按 RESP (REdis Serialization Protocol) 进行解析处理后,调用相关的命令进行处理。

调用命令的函数主要做两个事情:(1)查找对应的命令,比如这里的SET(2)调用该命令关联的函数进行处理,这里就是setCommand。

setCommand函数将客户端传进来的参数,变更数据库对应 KEY 的值,然后回复客户端。

回复客户端addReply函数将返回给客户端的内容,写到客户端变量的输出缓冲client.buf中,等待发送给客户端。

返回结果给客户端

以上是整个SET命令的事件处理,不过在这个时候,返回给用户的回复内容,只存放于服务器的客户端变量输出缓冲中。至于将结果返回给用户的过程,取决于版本,有不同的操作。

在 4.0 以前,每次的addReply操作会创建一个写事件,然后放到事件循环中执行。

而 4.0 开始,在每次重新进入一个新的循环之前,就是eventLoop->beforesleep();这个操作,Redis 会尝试直接发送给客户端,只有当发送的内容超过一定大小,无法一次发送完成的时候,才会去创建一个可写事件。

有兴趣的读者可以去看下 Redis 作者的这个 commit:

antirez in commit 1c7d87d:

Avoid installing the client write handler when possible.

目的是减少一次系统调用,适用于大部分操作类命令的回复。

可以观察到,整个操作的实现过程,和事件循环本身没有交集的(没有涉及到ae.c),开发者只需要关心具体命令的处理逻辑即可。

四、补充说明

事件都是来源于外部客户端吗?

这要看怎么定义“外部客户端”了。首先事件本身分为两种大类:文件事件和时间事件。本文主要介绍文件事件。而文件事件的产生可以是来源于网络客户端的连接,正如本文所描述的,也可以来自 Redis 集群内部运行需要,会使用一些伪客户端来触发一些文件事件。

举个例子,当有从节点 (slave/replica) 向主节点 (master) 发起一次同步的时候,在 Redis 就会产生一个需要处理同步数据的事件。不过严格意义上来讲,这个从节点对于主节点 Redis 来说,也属于“外部客户端”。正常情况下,Redis 自身不会主动产生文件事件。

Redis 是怎么定期更新状态、删除过期KEY的?

读者大概猜到我要引出时间事件这个概念了。Redis 会定期执行服务器的检查,以及一些周期操作,这个周期由参数hz决定,默认情况下是100毫秒触发一次检查,执行该周期内的时间事件。

时间事件 是 Redis 也是核心流程中重要的一个组成部分,限于篇幅不在这里详细介绍。但有了对事件循环的认识,要理解时间事件本身也不会太困难。

为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!同时小编了整理了一份还算完整的面试资料,希望能帮助到有需要的朋友,可以加企鹅 902570485(可直接点击群号进入)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341

推荐阅读更多精彩内容

  • Redis 网络架构及单线程模型 最近略有闲暇时间,于是对Redis进行了一些学习,学习途径除了官方文档还有Red...
    meng_philip123阅读 1,406评论 0 9
  • 综述 最近笔者阅读并研究redis源码,在redis客户端与服务器端交互这个内容点上,需要参考网上一些文章,但是遗...
    zbdba阅读 1,227评论 0 1
  • 一、事件 Redis服务器是一个事件驱动程序,服务器需要处理两类事件: 1)文件事件(file event)Red...
    稻壳_be03阅读 526评论 0 0
  • 文/婉悦悠然 碧湖之前世今生——目录 第十八章 中毒真相 第十九章 婉莹身世 一个青衣小生落在了皇后眼前。 他把皇...
    婉悦悠然阅读 622评论 17 17
  • 害怕冷却喜欢冬天,寒风袭来。 可能你现在身边有你的爱人, 他会捂住你的手放入他口袋。 可能现在你像我一样孤身一人,...
    三枚阅读 206评论 0 1