惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
V2EX
V
Visual Studio Blog
博客园_首页
Last Week in AI
Last Week in AI
Apple Machine Learning Research
Apple Machine Learning Research
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
SegmentFault 最新的问题
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Martin Fowler
Martin Fowler
Recent Announcements
Recent Announcements
J
Java Code Geeks
月光博客
月光博客
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
F
Fortinet All Blogs
P
Privacy & Cybersecurity Law Blog
C
CERT Recently Published Vulnerability Notes
C
CXSECURITY Database RSS Feed - CXSecurity.com
B
Blog RSS Feed
S
Schneier on Security
酷 壳 – CoolShell
酷 壳 – CoolShell
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
W
WeLiveSecurity
A
Arctic Wolf
U
Unit 42
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
有赞技术团队
有赞技术团队
Recorded Future
Recorded Future
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
大猫的无限游戏
大猫的无限游戏
Microsoft Security Blog
Microsoft Security Blog
Hacker News: Ask HN
Hacker News: Ask HN
量子位
B
Blog
T
The Exploit Database - CXSecurity.com
C
Cisco Blogs
博客园 - 三生石上(FineUI控件)
H
Help Net Security
博客园 - 叶小钗
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LINUX DO - 热门话题
Hugging Face - Blog
Hugging Face - Blog
Google DeepMind News
Google DeepMind News
小众软件
小众软件
雷峰网
雷峰网
TaoSecurity Blog
TaoSecurity Blog
Schneier on Security
Schneier on Security

犀利豆的博客

《SRE google 运维解密》读书笔记 (六) 《SRE google 运维解密》读书笔记 (五) 《SRE google 运维解密》读书笔记 (四) 《SRE google 运维解密》读书笔记 (三) 《SRE google 运维解密》读书笔记 (二) 《SRE google 运维解密》读书笔记 (一) 2021 总结 终于有一个 Java 可以用的微信机器人了 Vertx入门到实战—实现钉钉机器人内网穿透代理 钉钉机器人回调内网穿透代理--使用篇 周末补习(一)trie 树 那些有趣的代码(三)--勤俭持家的 ArrayList 那些有趣的代码(二)--偏不听父母话的 Tomcat 类加载器 那些有趣的代码(一)--有点萌的 Tomcat 的线程池 从需求第三定律说起--为什么知乎的回答质量下降了 如何利用 Spring Hibernate 高级特性设计实现一个权限系统 居然有人能忘记吃饭?写个微信机器人提醒他 我的2018年总结 从 LongAdder 中窥见并发组件的设计思路 徒手撸框架--实现 RPC 远程调用 我的写作工具链 Java 渲染 docx 文件,并生成 pdf 加水印 撸码的福音--变量名生成器的实现 Raft 协议学习笔记 dubbo 源码学习(一)开篇 Redis 命令的执行过程 Redis 数据库、键过期的实现 Redis 的基础数据结构(三)对象 Redis 的基础数据结构(二) 整数集合、跳跃表、压缩列表 Redis 的基础数据结构(一) 可变字符串、链表、字典 线程池 execute() 的工作逻辑 JAVA 中的 CAS 徒手撸框架--高并发环境下的请求合并 徒手撸框架--实现Aop 徒手撸框架--实现IoC 2017个人总结 最近遇到的几个问题集合 Redis RedLock 完美的分布式锁么? JAVA 8入门(二)流 JAVA 8入门(一)Lambda表达式 有道 Alfred Workflow 威力加强版 Kafka实现原理笔记 《交易系统:更新与跨越》读后笔记 Netty-Apns接入实现 Future研究 Hystrix入门研究 Redis实现分布式锁
Redis 中的事件驱动模型
Zhengxin Diao · 2018-03-23 · via 犀利豆的博客

原文地址:https://www.xilidou.com/2018/03/22/redis-event/

Redis 是一个事件驱动的内存数据库,服务器需要处理两种类型的事件。

  • 文件事件
  • 时间事件

下面就会介绍这两种事件的实现原理。

文件事件

Redis 服务器通过 socket 实现与客户端(或其他redis服务器)的交互,文件事件就是服务器对 socket 操作的抽象。 Redis 服务器,通过监听这些 socket 产生的文件事件并处理这些事件,实现对客户端调用的响应。

Reactor

Redis 基于 Reactor 模式开发了自己的事件处理器。

这里就先展开讲一讲 Reactor 模式。看下图:

reactor

“I/O 多路复用模块”会监听多个 FD ,当这些FD产生,accept,read,write 或 close 的文件事件。会向“文件事件分发器(dispatcher)”传送事件。

文件事件分发器(dispatcher)在收到事件之后,会根据事件的类型将事件分发给对应的 handler。

我们顺着图,从上到下的逐一讲解 Redis 是怎么实现这个 Reactor 模型的。

I/O 多路复用模块

Redis 的 I/O 多路复用模块,其实是封装了操作系统提供的 select,epoll,avport 和 kqueue 这些基础函数。向上层提供了一个统一的接口,屏蔽了底层实现的细节。

一般而言 Redis 都是部署到 Linux 系统上,所以我们就看看使用 Redis 是怎么利用 linux 提供的 epoll 实现I/O 多路复用。

首先看看 epoll 提供的三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22



int epoll_create(int size)







int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)








int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

再看 Redis 对文件事件,封装epoll向上提供的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39




typedef struct aeApiState {


int epfd;


struct epoll_event *events;

} aeApiState;




static int aeApiCreate(aeEventLoop *eventLoop)



static int aeApiResize(aeEventLoop *eventLoop, int setsize)



static void aeApiFree(aeEventLoop *eventLoop)



static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)



static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask)



static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)

所以看看这个ae_peoll.c 如何对 epoll 进行封装的:

  • aeApiCreate() 是对 epoll.epoll_create() 的封装。
  • aeApiAddEvent()aeApiDelEvent() 是对 epoll.epoll_ctl()的封装。
  • aeApiPoll() 是对 epoll_wait()的封装。

这样 Redis 的利用 epoll 实现的 I/O 复用器就比较清晰了。

再往上一层次我们需要看看 ea.c 是怎么封装的?

首先需要关注的是事件处理器的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

typedef struct aeFileEvent {




int mask;


aeFileProc *rfileProc;


aeFileProc *wfileProc;


void *clientData;

} aeFileEvent;

mask 就是可以理解为事件的类型。

除了使用 ae_peoll.c 提供的方法外,ae.c 还增加 “增删查” 的几个 API。

  • 增:aeCreateFileEvent
  • 删:aeDeleteFileEvent
  • 查: 查包括两个维度 aeGetFileEvents 获取某个 fd 的监听类型和aeWait等待某个fd 直到超时或者达到某个状态。

事件分发器(dispatcher)

Redis 的事件分发器 ae.c/aeProcessEvents 不但处理文件事件还处理时间事件,所以这里只贴与文件分发相关的出部分代码,dispather 根据 mask 调用不同的事件处理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {

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

int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0;


if (fe->mask & mask & AE_READABLE) {

rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}

if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}

processed++;
}

可以看到这个分发器,根据 mask 的不同将事件分别分发给了读事件和写事件。

文件事件处理器的类型

Redis 有大量的事件处理器类型,我们就讲解处理一个简单命令涉及到的三个处理器:

  • acceptTcpHandler 连接应答处理器,负责处理连接相关的事件,当有client 连接到Redis的时候们就会产生 AE_READABLE 事件。引发它执行。
  • readQueryFromClinet 命令请求处理器,负责读取通过 sokect 发送来的命令。
  • sendReplyToClient 命令回复处理器,当Redis处理完命令,就会产生 AE_WRITEABLE 事件,将数据回复给 client。

文件事件实现总结

我们按照开始给出的 Reactor 模型,从上到下讲解了文件事件处理器的实现,下面将会介绍时间时间的实现。

时间事件

Reids 有很多操作需要在给定的时间点进行处理,时间事件就是对这类定时任务的抽象。

先看时间事件的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26




typedef struct aeTimeEvent {


long long id;


long when_sec;
long when_ms;


aeTimeProc *timeProc;


aeEventFinalizerProc *finalizerProc;


void *clientData;


struct aeTimeEvent *next;

} aeTimeEvent;

看见 next 我们就知道这个 aeTimeEvent 是一个链表结构。看图:

timeEvent

注意这是一个按照id倒序排列的链表,并没有按照事件顺序排序。

processTimeEvent

Redis 使用这个函数处理所有的时间事件,我们整理一下执行思路:

  1. 记录最新一次执行这个函数的时间,用于处理系统时间被修改产生的问题。
  2. 遍历链表找出所有 when_sec 和 when_ms 小于现在时间的事件。
  3. 执行事件对应的处理函数。
  4. 检查事件类型,如果是周期事件则刷新该事件下一次的执行事件。
  5. 否则从列表中删除事件。

综合调度器(aeProcessEvents)

综合调度器是 Redis 统一处理所有事件的地方。我们梳理一下这个函数的简单逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

shortest = aeSearchNearestTimer(eventLoop);


timeval = shortest - nowTime;


if(timeval < 0){
timeval = 0
}


numevents = aeApiPoll(eventLoop, timeval);


if (AE_READABLE) {

rfileProc(eventLoop,fd,fe->clientData,mask);
}

if (AE_WRITABLE) {
wfileProc(eventLoop,fd,fe->clientData,mask);
}

以上的伪代码就是整个 Redis 事件处理器的逻辑。

我们可以再看看谁执行了这个 aeProcessEvents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

void aeMain(aeEventLoop *eventLoop) {

eventLoop->stop = 0;

while (!eventLoop->stop) {


if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);


aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}

然后我们再看看是谁调用了 eaMain:

1
2
3
4
5
6
7
8
int main(int argc, char **argv) {

...
aeMain(server.el);


...
}

我们在 Redis 的 main 方法中找个了它。

这个时候我们整理出的思路就是:

  • Redis 的 main() 方法执行了一些配置和准备以后就调用 eaMain() 方法。

  • eaMain() while(true) 的调用 aeProcessEvents()

所以我们说 Redis 是一个事件驱动的程序,期间我们发现,Redis 没有 fork 过任何线程。所以也可以说 Redis 是一个基于事件驱动的单线程应用。

总结

在后端的面试中 Redis 总是一个或多或少会问到的问题。

读完这篇文章你也许就能回答这几个问题:

  • 为什么 Redis 是一个单线程应用?
  • 为什么 Redis 是一个单线程应用,却有如此高的性能?

如果你用本文提供的知识点回答这两个问题,一定会在面试官心中留下一个高大的形象。

大家还可以阅读我的 Redis 相关的文章:

Redis 的基础数据结构(一) 可变字符串、链表、字典

Redis 的基础数据结构(二) 整数集合、跳跃表、压缩列表

Redis 的基础数据结构(三)对象

Redis 数据库、键过期的实现

欢迎关注我的微信公众号:
二维码