Redis为何这么快?

| 做者 吴显坚,腾讯云数据库高级工程师,参与过360开源项目Pika的研发工做,现从事redis数据库研发工做。redis


Redis服务器是一个事件驱动程序, 事件是Redis服务器的核心, 它处理两项重要的任务, 一个是IO事件(文件事件), 另一个是时间事件. Redis服务器经过套接字与客户端进行链接, 而文件事件能够理解为服务器对套接字操做的抽象. 服务器与客户端的通讯会产生相应的文件事件, 而服务器则经过监听并处理这些事件来完成一系列网络通讯操做. 另外Redis内部有一些操做(从Redis4.0的代码分析目前时间事件只有serverCron)须要在给定的时间点执行, 而时间事件就是Redis服务器对这类定时操做的抽象。数据库

1、aeEventLoop数组

在分析具体代码以前, 咱们先了解一下在事件处理中处于核心部分的aeEventLoop究竟是什么:安全

/* State of an event based program */

建立aeEventLoop只须要一个setsize参数, 它标识了当前aeEventLoop最大能够监听的文件描述符数(一般redis传入server.maxclients+CONFIG_FDSET_INCR,也就是在用户指定的最大客户端链接数的基础上再额外增长128, 这128能够用于Redis内部打开AOF,RDB文件以及主从, 集群互相通讯所对应的文件句柄), 建立aeEventLoop时, aeFileEvent和aeFiredEvent数组的大小就由setsize肯定。服务器

1. aeFileEvent网络

内部以掩码的形式存储了当前套接字关心的事件(可读/可写事件), 内部还有两个函数指针指向可读/可写事件发生时应该调用的函数, 另外还有一个无类型的指针指向相关联的数据, 这里须要注意的是, events是一个数组, 而套接字就是做为下标来进行索引对应aeFileEvent, 例如我当前关心的套接字是9, 那么events[9]就是它对应的文件事件数据结构(csapp中提到过, 当咱们调用系统函数返回描述符数字时, 返回的描述符老是在进程中当前没有打开的最小描述符, 因此咱们无需担忧文件描述符被反复的建立销毁, 而愈来愈大的问题)。数据结构

2. aeFiredEventapp

内部以掩码的形式存储了当前已经触发的事件和对应的套接字, 实际上fired数组只有在调用aeApiPoll的时候才会被赋值, 例如当前发现有套接字6, 8有可读事件, 而套接字10有可写事件, 那么fired数组的前三个元素会被赋值{fd = 6, mask =AE_READABLE}, {fd = 8, mask = AE_READABLE}, {fd = 10, mask = AE_WRITABLE}, 紧接着咱们以6为索引, 找到文件事件数据结构events[6],而后发现触发的是可读事件, 咱们再调用events[6]中rfileProc来处理可读事件。函数

aeEventLoop *aeCreateEventLoop(int setsize){

对于时间事件, aeEventLoop中有一个timeEventHead指针指向第一个时间事件, 因为aeEventLoop建立之初, 内部没有任什么时候间事件, 因此初始化时timeEventHead指向NULL, 每当有新的时间事件时, 总会被添加到timeEventHead头部, 因为aeTimeEvent结构体中有next指针能够指向下一个aeTimeEvent结构体, 因此只要咱们获取timeEventHead就能遍历当前全部的时间事件了, 另外有一个细节须要注意, 最后一个aeTimeEvent结构体中的next指针指向的是timeEventHead, 因此全部时间事件实际上就是由一个环形链表串连起来的。oop

image.png

2、文件事件

在介绍中有提到过文件事件实际上就是服务器对套接字操做的抽象, 当套接字有可读\写事件触发的时候, 咱们须要调用相应的处理函数, 下面先看一下跟文件事件相关的结构体:

/* File event structure */

在aeEventLoop初始化的时候会为aeFileEvent数组(events)分配空间, 数组的大小由参数setsize指定,代表了当前Redis最大打开的套接字的大小, 套接字与aeFileEvent一一对应, 也就是说咱们能够经过套接字数值做为索引到events数组中找到他对应的aeFileEvent对象。

当咱们在aeEventLoop中注册一个文件事件时, 首先咱们判断传入的套接字对events数组是否有越界行为, 若没有越界行为, 咱们即可以获取与当前套接字对应的aeFileEvent对象, 而后调用aeApiAddEvent将当前的文件描述符以及监听的事件注册到底层IO多路复用机制(epoll, select, evport, kqueue其中之一)中, 另外咱们还须要指定当可读/可写事件发生时须要调用的函数,另外当前文件事件的一些私有数据被存放在clientData指向的对象当中。

int aeCreateFileEvent(aeEventLoop *eventLoop,int fd, int mask,

3、时间事件

Redis内部的时间事件实际能够分为两类, 一类是定时事件, 也就是须要在将来某一个时间点触发的事件(只触发一次), 另一类是周期性事件,和前面的定时事件只触发一次不一样, 周期性事件是每隔一段时间又会从新触发一次。

Redis使用了timeProc指向函数的返回值来判断当前属于哪类事件, 若函数返回AE_NOMORE(也就是-1),说明当前事件无需再次触发(将id置删除标记AE_DELETED_EVENT_ID), 若函数返回一个大于等于0的值n, 说明再等待n秒, 该事件须要再从新被触发(根据返回值更新when_sec和when_ms),在博客开头提到的serverCron时间事件实际上就是一个周期性事件, 函数末尾会返回1000/server.hz, server.hz默认被设置为10, 也就是说serverCron平均每间隔100ms会被调用一次。

/* Time event structure */

Redis调用aeCreateTimeEvent来建立一个时间任务, 实现很是简单, 传参咱们关注一下milliseconds和proc便可, 前者指定了时间事件距离当前的触发时间, 后者指定了时间事件触发时应调用的函数, 内部经过aeAddMillisecondsToNow将当前定时任务触发的时间戳计算出来赋值给when_sec和when_ms, 而后再将timeProc指向时间事件到达时应该调用的函数。

在完成了aeTimeEvent结构体内部变量赋值以后, 最后将其添加到aeEventLoop内部的存储定时间事件的环形链表的头部中(这里须要注意的是, 因为咱们老是将新的时间事件加入环形链表的头部, 因此时间事件触发的时间前后并非在环形链表中有序的, 咱们须要将环形链表遍历完毕才能保证当前已经到达的时间事件都已经被处理完毕, 不过因为在开头提到过, 目前Redis只存在serverCron一个时间事件, 因此咱们无需担忧遍历环形链表影响服务性能), 此时一个时间事件就算建立完成了。

static void aeAddMillisecondsToNow(long longmilliseconds, long *sec, long *ms) {

Redis经过aeDeleteTimeEvent函数来删除一个时间任务, 传参只有一个待删除时间事件的id, 咱们发现这里的删除其实是一种惰性删除, 将aeTimeEvent中的id标记为AE_DELETED_EVENT_ID, 而不是直接将aeTimeEvent对象从链表中删除而且释放, 我的认为这么实现的缘由更可能是为了安全考虑以及代码的简洁性, 考虑在一个时间事件中原本想删除另一个时间事件, 可是因为id填错, 误删成本身了, 此时若是释放自身aeTimeEvent对象, 这是十分危险的。

int aeDeleteTimeEvent(aeEventLoop *eventLoop,long long id)

4、事件的调度与执行

Redis是单线程的, 内部是一直处于aeMain中的while循环中, 而循环内部不断调用aeProcessEvents函数, 该函数会对上面提到的文件事件和时间事件进行调度, 决定什么时候处理文件事件以及时间事件。

void aeMain(aeEventLoop *eventLoop) {

实际上aeProcessEvents函数内部作的事情也很是简单, 下面进行了梳理:

1. 首先调用aeSearchNearestTimer获取到达时间距离当前最近的时间事件;

2. 计算上一步获取到的时间事件还有多久才能够触发, 而且将结果记录到一个struct timeval*指针指向的结构体中(若在步骤一中没有获取到时间事件对象, 那么指针为NULL);

3. 阻塞并等待文件事件的产生, 最大的阻塞时间由步骤二决定(步骤二指针为NULL的场景表示当前没有时间事件, 咱们能够永远阻塞, 直到有文件事件到达);

4. 若是在最大阻塞时间内获取到了文件事件, 则根据文件事件的类型调用对应的读事件处理函数或者写事件处理函数;

5. 遍历时间事件链表, 在这个过程当中可能会遇到id为AE_DELETED_EVENT_ID的表明已经作了删除标记的时间事件, 须要将该时间事件从链表中移除, 而且进行释放, 如遇到已经达到的时间事件, 则调用其绑定的处理函数, 而且根据返回值来判断该事件时间是否须要在给定的时间内再从新触发。

5、问题

Q1: 时间事件触发的时间必定精准么?

A1: 时间事件的触发并不能在指定的时间精准触发, 通常都要比指定的时间稍晚一点, 此外在Redis单线程模型下, 时间事件都是串行执行的, 中间若是某个时间事件处理时间长, 更加影响了后面时间事件执行时间的精准性. 并且时间事件链表是无序的, 因此在极端场景下, 存在优先级低的时间事件比优先级高的事件先触发的可能性, 不过好在目前Redis内部只有一个时间事件, 因此影响不会太大.


Q2: aeEventLoop在建立之初就指定了可监听文件描述符的数量, 以后又经过config set maxclients命令动态调整客户端最大链接数是怎么实现的?

A2: 经过翻看源码了解到, aeEventLoop提供了aeResizeSetSize函数, 用户从新分配events和fired数组的大小, 使aeEventLoop可监听的套接字数量得以调整, 当新的maxclients比原先要大的时候, 会调用该函数, 扩大aeEventLoop可监听文件描述符的数量, 以支持更多的客户端链接.

int aeResizeSetSize(aeEventLoop *eventLoop,int setsize) {

6、总结

Redis对事件的处理方式十分巧妙, 文件事件和时间事件之间相互配合, 充分的利用时间事件达到以前的这段时间等待和处理文件事件, 这样既避免了CPU的空转检查, 也能及时的处理文件事件. 此外经过时间事件中timeProc函数的返回值, 将时间事件的移除和再次触发权彻底交给了用户, 使用起来更加灵活.

本文由博客一文多发平台 OpenWrite 发布!

相关文章
相关标签/搜索