Redis中事件驱动模型示例详解
前言
Redis 是一个事件驱动的内存数据库,服务器需要处理两种类型的事件。
- 文件事件
- 时间事件
下面就会介绍这两种事件的实现原理。
文件事件
Redis 服务器通过 socket 实现与客户端(或其他redis服务器)的交互,文件事件就是服务器对 socket 操作的抽象。 Redis 服务器,通过监听这些 socket 产生的文件事件并处理这些事件,实现对客户端调用的响应。
Reactor
Redis 基于 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 提供的三个方法:
/* * 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大 */ int epoll_create(int size); /* * 可以理解为,增删改 fd 需要监听的事件 * epfd 是 epoll_create() 创建的句柄。 * op 表示 增删改 * epoll_event 表示需要监听的事件,Redis 只用到了可读,可写,错误,挂断 四个状态 */ int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); /* * 可以理解为查询符合条件的事件 * epfd 是 epoll_create() 创建的句柄。 * epoll_event 用来存放从内核得到事件的集合 * maxevents 获取的最大事件数 * timeout 等待超时时间 */ int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
再看 Redis 对文件事件,封装epoll向上提供的接口:
/* * 事件状态 */ typedef struct aeApiState { // epoll_event 实例描述符 int epfd; // 事件槽 struct epoll_event *events; } aeApiState; /* * 创建一个新的 epoll */ static int aeApiCreate(aeEventLoop *eventLoop) /* * 调整事件槽的大小 */ static int aeApiResize(aeEventLoop *eventLoop, int setsize) /* * 释放 epoll 实例和事件槽 */ static void aeApiFree(aeEventLoop *eventLoop) /* * 关联给定事件到 fd */ static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) /* * 从 fd 中删除给定事件 */ 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 是怎么封装的?
首先需要关注的是事件处理器的数据结构:
typedef struct aeFileEvent { // 监听事件类型掩码, // 值可以是 AE_READABLE 或 AE_WRITABLE , // 或者 AE_READABLE | AE_WRITABLE int mask; /* one of AE_(READABLE|WRITABLE) */ // 读事件处理器 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 调用不同的事件处理器。
//从 epoll 中获关注的事件 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 确保读/写事件只能执行其中一个 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 有很多操作需要在给定的时间点进行处理,时间事件就是对这类定时任务的抽象。
先看时间事件的数据结构:
/* Time event structure * * 时间事件结构 */ typedef struct aeTimeEvent { // 时间事件的唯一标识符 long long id; /* time event identifier. */ // 事件的到达时间 long when_sec; /* seconds */ long when_ms; /* milliseconds */ // 事件处理函数 aeTimeProc *timeProc; // 事件释放函数 aeEventFinalizerProc *finalizerProc; // 多路复用库的私有数据 void *clientData; // 指向下个时间事件结构,形成链表 struct aeTimeEvent *next; } aeTimeEvent;
看见 next 我们就知道这个 aeTimeEvent 是一个链表结构。看图:
注意:这是一个按照id倒序排列的链表,并没有按照事件顺序排序。
processTimeEvent
Redis 使用这个函数处理所有的时间事件,我们整理一下执行思路:
- 记录最新一次执行这个函数的时间,用于处理系统时间被修改产生的问题。
- 遍历链表找出所有 when_sec 和 when_ms 小于现在时间的事件。
- 执行事件对应的处理函数。
- 检查事件类型,如果是周期事件则刷新该事件下一次的执行事件。
- 否则从列表中删除事件。
综合调度器(aeProcessEvents)
综合调度器是 Redis 统一处理所有事件的地方。我们梳理一下这个函数的简单逻辑:
// 1. 获取离当前时间最近的时间事件 shortest = aeSearchNearestTimer(eventLoop); // 2. 获取间隔时间 timeval = shortest - nowTime; // 如果timeval 小于 0,说明已经有需要执行的时间事件了。 if(timeval < 0){ timeval = 0 } // 3. 在 timeval 时间内,取出文件事件。 numevents = aeApiPoll(eventLoop, timeval); // 4.根据文件事件的类型指定不同的文件处理器 if (AE_READABLE) { // 读事件 rfileProc(eventLoop,fd,fe->clientData,mask); } // 写事件 if (AE_WRITABLE) { wfileProc(eventLoop,fd,fe->clientData,mask); }
以上的伪代码就是整个 Redis 事件处理器的逻辑。
我们可以再看看谁执行了这个 aeProcessEvents:
void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { // 如果有需要在事件处理前执行的函数,那么运行它 if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop); // 开始处理事件 aeProcessEvents(eventLoop, AE_ALL_EVENTS); } }
然后我们再看看是谁调用了 eaMain:
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 是一个单线程应用,却有如此高的性能?
如果你用本文提供的知识点回答这两个问题,一定会在面试官心中留下一个高大的形象。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。
- linux(十三)之磁盘分区、创建文件系统、挂载
- LINQ驱动数据的查询功能
- JS魔法堂:jQuery.Deferred(jQuery1.5-2.1)源码剖析
- 一步一步学习Bootstrap系列--表单布局
- Java进阶01 String类
- linux(十二)之用户管理
- Dapper.Net 应用
- linux(十)配置ssh免密登录实现
- 微信群也有群相册功能了,终于可以给手机相册腾出空间了!
- PHP5.3、PHP5.4安装ZendOptimizer
- linux(十一)之初始化文件
- C编译: 动态连接库 (.so文件)
- 腾讯叮当首都发布生态合作伙伴计划,全面开放AI能力
- Python火爆的背后的应用领域是数据挖掘、大数据和人工智能的应用吗?
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 剑指Offer LeetCode 面试题25. 合并两个排序的链表
- LeetCode 20200601 打卡 1431. 拥有最多糖果的孩子
- 剑指Offer LeetCode 面试题24. 反转链表
- 剑指Offer LeetCode 面试题22. 链表中倒数第k个节点
- 剑指Offer LeetCode 面试题18. 删除链表的节点
- 剑指Offer LeetCode 面试题06. 从尾到头打印链表
- 最详细的docker中安装并配置redis
- 剑指Offer LeetCode 面试题59 - I. 滑动窗口的最大
- 剑指Offer LeetCode 面试题58 - II. 左旋转字符串
- 剑指Offer LeetCode 面试题58 - I. 翻转单词顺序
- 剑指Offer LeetCode 面试题56 - II. 数组中数字出现的次数 II
- 站在软件工程的角度重新思考面向对象(含高清图谱)
- 剑指Offer LeetCode 面试题57. 和为s的两个数字
- 白嫖Layui树型可折叠,可自定义,可搜索表格的实例
- 剑指Offer LeetCode 面试题53 - II. 0~n-1中缺失的数字