探索Redis设计与实现10:Redis的事件驱动模型与命令执行过程
本文转自互联网
本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看
喜欢的话麻烦点下Star哈
文章首发于我的个人博客:
www.how2playlife.com
本文是微信公众号【Java技术江湖】的《探索Redis设计与实现》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。
该系列博文会告诉你如何从入门到进阶,Redis基本的使用方法,Redis的基本数据结构,以及一些进阶的使用方法,同时也需要进一步了解Redis的底层数据结构,再接着,还会带来Redis主从复制、集群、分布式锁等方面的相关内容,以及作为缓存的一些使用方法和注意事项,以便让你更完整地了解整个Redis相关的技术体系,形成自己的知识框架。
如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。
原文地址: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 提供的三个方法:
/*
* 创建一个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 是一个链表结构。看图:
timeEvent
注意这是一个按照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 是一个单线程应用,却有如此高的性能?
如果你用本文提供的知识点回答这两个问题,一定会在面试官心中留下一个高大的形象。
大家还可以阅读我的 Redis 相关的文章:
探索Redis设计与实现10:Redis的事件驱动模型与命令执行过程的更多相关文章
- Redis 命令执行过程(上)
今天我们来了解一下 Redis 命令执行的过程.在之前的文章中<当 Redis 发生高延迟时,到底发生了什么>我们曾简单的描述了一条命令的执行过程,本篇文章展示深入说明一下,加深读者对 R ...
- Redis 命令执行过程(下)
在上一篇文章中<Redis 命令执行过程(上)>中,我们首先了解 Redis 命令执行的整体流程,然后细致分析了从 Redis 启动到建立 socket 连接,再到读取 socket 数据 ...
- Redis 设计与实现 10:五大数据类型之有序集合
有序集合 sorted set (下面我们叫zset 吧) 有两种编码方式:压缩列表 ziplist 和跳表 skiplist. 编码一:ziplist zset 在 ziplist 中,成员(mem ...
- Redis 设计与实现:Redis 对象
本文的分析都是基于 Redis 6.0 版本源码 redis 6.0 源码:https://github.com/redis/redis/tree/6.0 在 Redis 中,有五大数据类型,都统一封 ...
- redis学习笔记——expire、pexpire、expireat、pexpireat的执行过程
这里主要讲的Redis是怎么样设置过期键的,可以算作后续"Redis过期键的删除策略"的前篇或者说预备知识. 在了解过期键问题前我们首先需要对redis的数据库和数据库键空间有一定 ...
- Redis 源码走读(一)事件驱动机制与命令处理
eventloop 从 server.c 的 main 方法看起 int main(int argc, char **argv) { ....... aeSetBeforeSleepProc(serv ...
- Redis设计与实现(一~五整合版)【搬运】
Redis设计与实现(一~五整合版) by @飘过的小牛 一 前言 项目中用到了redis,但用到的都是最最基本的功能,比如简单的slave机制,数据结构只使用了字符串.但是一直听说redis是一个很 ...
- Redis入门指南(第2版) Redis设计思路学习与总结
https://www.qcloud.com/community/article/222 宋增宽,腾讯工程师,16年毕业加入腾讯,从事海量服务后台设计与研发工作,现在负责QQ群后台等项目,喜欢研究技术 ...
- Redis设计思路学习与总结
版权声明:本文由宋增宽原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/222 来源:腾云阁 https://www.qclo ...
随机推荐
- LOJ 3094 「BJOI2019」删数——角标偏移的线段树
题目:https://loj.ac/problem/3094 弱化版是 AGC017C . 用线段树维护那个题里的序列即可. 对应关系大概是: 真实值的范围是 [ 1-m , n+m ] :考虑设偏移 ...
- [CSP-S模拟测试]:Tourist Attractions(简单图论+bitset)
题目描述 在美丽的比特镇一共有$n$个景区,编号依次为$1$到$n$,它们之间通过若干条双向道路连接.$Byteasar$慕名来到了比特镇旅游,不过由于昂贵的门票费,他只能负担起$4$个景区的门票费. ...
- docstoc对Scribd的威胁比SlideShare还要大。
docstoc,这是在TechCrunch40互联网交流会上崭露头角的个性化服务.docstoc为用户提供了在线存储.分享以及交流文档的互联网服务.与Scribd相同的是,除了文档分享功能以外,doc ...
- Hive date_add 和 date_diff 函数
date_add 函数 例子:date_add('day',-1,current_date) date_diff 函数 例子:date_diff('day',cast(a.dt1 as timesta ...
- 三线SWD模式Jlink
三线SWD模式Jlink 在公司实习,部门经理让我做一个USB-CAN的适配器. 在网上找资料,找到一个开源的USB-CAN的适配器的资料. 采用的是CP2102芯片实现USB转串口.STM32作 ...
- 如何消去delphi Stringgrid重绘时产生重影
procedure TForm1.Stringgrid1DrawCell(Sender: TObject; ACol, ARow: Integer; Rect: TRect; State: TGrid ...
- sqlserver怎么copy一张表为另一张表
SQL Server中,如果目标表存在:insert into 目标表 select * from 原表;SQL Server中,,如果目标表不存在:select * into 目标表 from 原表 ...
- [Codeforces 729F] Financiers Game
题意: 两个人分别从长度为n的数列的两端开始取数,如果前一 个人取了k个数,后一个人必须取k或k+1个. 第一个人最 开始可以取1个或2个,不能操作时结束. 两个人都希望自 己取到的数字之和尽量大,并 ...
- select的限制与poll的使用
select的限制 select的并发数受到两个限制:1.一个进程能打开的最大描述符数量;2.select中fd_set集合容量的限制(FD_SETSIZE) 关于进程的最大描述符数量: ulimit ...
- python-javascript之dom
DOM DOM:(document object mode)文档对象模型.DOM为文档提供了结构化表示,并定义了如何通过脚本来访问文档结构. 目的就是为了能让js操作html元素而制定的一个规范 DO ...