Redis源码阅读【8-命令处理生命周期-2】

Redis源码阅读【1-简单动态字符串】
Redis源码阅读【2-跳跃表】
Redis源码阅读【3-Redis编译与GDB调试】
Redis源码阅读【4-压缩列表】
Redis源码阅读【5-字典】
Redis源码阅读【6-整数集合】
Redis源码阅读【7-quicklist】
Redis源码阅读【8-命令处理生命周期-1】
Redis源码阅读【8-命令处理生命周期-2】
Redis源码阅读【8-命令处理生命周期-3】
Redis源码阅读【8-命令处理生命周期-4】
Redis源码阅读【番外篇-Redis的多线程】
建议搭配源码阅读源码地址

1、介绍

  • Redis是典型的事件驱动型服务,而事件分为文件事件socket的可独写事件)与时间事件(定时任务)两大类。

2、事件处理

无论是文件事件还是时间事件都封装在aeEventLoop中,代码如下:

typedef struct aeEventLoop {
    int maxfd;   //已经接受的最大的文件描述符
    int setsize; //当前循环中所能容纳的文件描述符的数量 
    long long timeEventNextId;//下一个时间事件的ID
    time_t lastTime;//上一次被访问的时间,用来检测系统时钟是否被修改   
    aeFileEvent *events; //文件事件数组 
    aeFiredEvent *fired; //被触发的文件事件 
    aeTimeEvent *timeEventHead; //事件实现的head,是一个链表
    int stop; //标识事件是否停止
    void *apidata; //对kqueue epoll select等封装 类型是 aeApiState
    aeBeforeSleepProc *beforesleep;//用于阻塞的函数
    aeBeforeSleepProc *aftersleep;//用于唤醒的函数
    int flags; //事件类型标志
} aeEventLoop;

stop 和 events标识事件循环是否结束,events为文件事件数组,存储已经注册的文件事件;
fired存储被触发的文件事件;
timeEventHead Redis有多个定时任务,因此理论上应该有多个时间事件,多个时间事件形成链表,timeEventHead即为时间事件链表头节点;
beforesleep 和 aftersleep Redis服务器需要阻塞等待文件事件的发生,进程阻塞之前会调用beforesleep函数,进程因为某种原因被唤醒之后会调用aftersleep函数;
apidata Redis底层可以使用4种I/O多路复用模型(kqueue epoll select等),apidata是对这4种模型的封装,对应的类型是aeApiState
timeEventNextId 时间事件是一个链表,那么timeEventNextId记录的是下一个需要执行的时间事件ID;
lastTime 用来记录上一次被访问的时间,用来检测系统时钟是否被修改;
maxfd 已经接受的最大的文件描述符;
setsize当前循环中所能容纳的文件描述符的数量 ;
flags用来标记当前事件类型,对应的枚举内容如下:

//flags的定义
#define AE_FILE_EVENTS 1
#define AE_TIME_EVENTS 2
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)
#define AE_DONT_WAIT 4
#define AE_CALL_AFTER_SLEEP 8
  • 事件驱动程序一般来说都是一直循环执行下去的,没错Redis也是这样,事件在一个循环里面循环等待事件的发生并处理,当然这个循环不是死循环,是有条件的,代码如下:
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0; //设置停止标记为(不停止)
    while (!eventLoop->stop) { //除非停止标志被设置,不然循环不会停止
        if (eventLoop->beforesleep != NULL) 
            eventLoop->beforesleep(eventLoop);//函数不为空,先执行阻塞函数
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);//执行事件
    }
}
  • 函数aeProcessEvents为事件处理主函数,其中第二个参数是一个标志flagsAE_ALL_EVENTS标识函数需要处理文件事件与时间事件,AE_CALL_AFTER_SLEEP表示阻塞等待文件事件需要执行aftersleep函数。

3、文件事件

  • Redis客户端通过TCP socket 与服务器端进行交互,文件事件指的就是socket的可读可写事件。socket独写操作有阻塞非阻塞之分。采用的是阻塞模式时,一个进程只能处理一条网络连接的独写请求,为了同时处理多条网络连接,通常会采用多线程或者多进程的方式;非阻塞模式下,可以使用目前比较成熟的I/O多路复用模型,比如 select/epoll/kqueue等。视不同的操作系统而定。
  • 基于Linux系统,主要还是使用epollepollLinux内核为处理大量并发网络连接而提出的解决方案,能显著提升操作系统CPU利用率 。eopll使用非常简单,总共有3个API:epoll_create函数创建一个epoll专用的文件描述符,用于后续epoll相关API调用;epoll_ctl函数向epoll注册,修改或删除需要监控的事件;epoll_wait函数会阻塞进程,直到监控的若干网络连接有事件发生。函数定义代码如下:
//创建 epoll
int epoll_create (int __size);
//注册 epoll事件
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)
  • epoll_create 输入的size通知内核程序期望注册的网络连接数量,内核以此判断初始分配空间大小;注意再Linux2.6.8版本之后,内核动态分配空间,此参数会被忽略。返回参数为epoll专用的文件描述符,不再使用的时候需要关闭此文件描述符。

  • epoll_ctl函数执行成功返回0,否则返回-1,错误码设置在变量errno里面,输入参数含义如下:

epfd 函数epoll_create返回的epoll文件描述符。
op 需要进行的操作,EPOLL_CTL_ADD表示注册事件,EPOLL_CTL_MOD表示修改网络连接事件,EPOLL_CTL_DEL表示删除事件,事件定义如下:

#define EPOLL_CTL_ADD 1	/* Add a file descriptor to the interface.  */
#define EPOLL_CTL_DEL 2	/* Remove a file descriptor from the interface.  */
#define EPOLL_CTL_MOD 3	/* Change file descriptor epoll_event structure.  */

fd 网络连接的socket文件描述符。
event 需要监控的事件,结构体epoll_event定义如下:

struct epoll_event
{
  uint32_t events;	//epoll 中包含的事件数组
  epoll_data_t data; //data 保存与文件描述符关联的数据
} __EPOLL_PACKED;

typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

  • 其中events表示需要监控的事件类型,比较常用的是EPOLLIN 文件描述符可读事件,EPOLLOUT文件描述符可写事件;data保存于文件描述关联的数据。
    epoll事件定义如下:
enum EPOLL_EVENTS
  {
    EPOLLIN = 0x001,
#define EPOLLIN EPOLLIN
    EPOLLPRI = 0x002,
#define EPOLLPRI EPOLLPRI
    EPOLLOUT = 0x004,
#define EPOLLOUT EPOLLOUT
    EPOLLRDNORM = 0x040,
#define EPOLLRDNORM EPOLLRDNORM
    EPOLLRDBAND = 0x080,
#define EPOLLRDBAND EPOLLRDBAND
    EPOLLWRNORM = 0x100,
#define EPOLLWRNORM EPOLLWRNORM
    EPOLLWRBAND = 0x200,
#define EPOLLWRBAND EPOLLWRBAND
    EPOLLMSG = 0x400,
#define EPOLLMSG EPOLLMSG
    EPOLLERR = 0x008,
#define EPOLLERR EPOLLERR
    EPOLLHUP = 0x010,
#define EPOLLHUP EPOLLHUP
    EPOLLRDHUP = 0x2000,
#define EPOLLRDHUP EPOLLRDHUP
    EPOLLEXCLUSIVE = 1u << 28,
#define EPOLLEXCLUSIVE EPOLLEXCLUSIVE
    EPOLLWAKEUP = 1u << 29,
#define EPOLLWAKEUP EPOLLWAKEUP
    EPOLLONESHOT = 1u << 30,
#define EPOLLONESHOT EPOLLONESHOT
    EPOLLET = 1u << 31
#define EPOLLET EPOLLET
  };
  • epoll_wait 函数执行成功时返回0,否则返回-1,错误码设置在变量errno;输入参数含义如下:

epfd 函数epoll_create返回的epoll文件描述符;
epoll_event 作为输出参数使用,用于回传已触发的事件数组;
maxevents 每次能处理的最大事件数目;
timeout epoll_wait函数阻塞超时事件,如果超过timeout时间事件还没发生,函数不再阻塞直接返回;当timeout设置为0时函数立即返回,timeout设置为-1时函数会一直阻塞到有事件发生;

  • Redis并没有直接使用epoll提供的API,而是同时支持4种I/O多路复用模型,并将这些模型的API进一步统一封装,由文件ae_evport.cae_epoll.cae_kqueue.cae_select.c 实现。

  • Redis在编译的时候会检查操作系统支持的I/O多路复模型来选择使用哪种。

  • epoll为例aeApiCreate函数是对epoll_create的封装;aeApiAddEvent函数用于添加事件,是对epoll_ctl的封装;aeApiDelEvent函数用于删除事件,是对epoll_ctl的封装;aeApiPoll是对epoll_wait的封装。此外还有aeApiResizeaeApiFree,是综合封装用于扩展和完全释放使用。函数定义如下:

static int aeApiCreate(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); 
static int aeApiResize(aeEventLoop *eventLoop, int setsize);//调整事件循环的最大设置大小
static void aeApiFree(aeEventLoop *eventLoop);

这些函数定义的入参含义如下:
eventLoop 事件循环,与文件事件相关的最主要字段有三个,apidata指向I/O多路复用模型对象,注意4种I/O多路复用对象的类型不同,因此此字段是void*类型;events存储需要监控的事件数组,以socket文件描述符作为数组索引存取元素;fired存储已触发的事件数组。

  • epoll为例,apidata字段指向的I/O多路复用模型对象定义如下:
typedef struct aeApiState {
    int epfd; //函数epoll_create返回的**epoll**文件描述符;
    struct epoll_event *events; //需要监控的事件类型
} aeApiState;
  • 其中epfd 函数epoll_create返回的epoll文件描述符,events存储epoll_wait函数返回时已触发的事件数组。
    fd 操作的socket文件描述符;
    mask 或 delmask 添加或者删除的事件类型,AE_NONE表示没有任何事件;AE_READABLE表示可读事件;AE_WRITABLE表示可写事件;
    tvp 阻塞等待文件事件的超时时间。

  • 正常运行的Redis服务器,aeApiPoll应该是调用最多的方法之一,现在来介绍一些其内部的具体实现:

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
	//获取对应的aeApiState类型
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;
    //阻塞等待事件的发生
    retval = epoll_wait(state->epfd, state->events, eventLoop->setsize,
                        tvp ? (tvp->tv_sec * 1000 + tvp->tv_usec / 1000) : -1);
    if (retval > 0) {
        int j;
        //所有发生的事件数量
        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events + j;
            //转换事件类型为Redis定义的类型(比如:读,写等操作)参考 EPOLL_EVENTS枚举
            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE | AE_READABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE | AE_READABLE;
            //记录发生事件到fired数组(保存下来慢慢执行)
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}
  • 函数首先需要通过eventLoop->apidata 字段获取 epoll模型对应的aeApiState结构体对象,才能调用epoll_wait函数等待事件的发生;epoll_wait函数将已触发的事件存储到aeApiState对象的events字段,Redis再次遍历所有已触发事件,将其封装在eventLoop->fired数组,数组元素类型为结构体aeFiredEvent,只有两个字段,fd表示发生事件的socket文件描述符,mask表示发生的事件类型,如AE_READABLE可读事件和AE_WRITABLE可写事件。

  • 此外通过接口的对接我们可以切换不同类型的I/O多路复用模型,比如文件ae_select.cae_epoll.c中都实现了static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)这个方法。这种方式可以类比java里面的接口,通过这种方式,我们可以无缝的切换各种类型的I/O多路复用模型:
    在这里插入图片描述

  • 上面简单介绍了epoll的使用,以及Redisepoll等IO多路复用模型的封装,下面我们回到本节的主题,文件事件。结构体aeEventLoop有一个关键字段events,类型为aeFileEvent数组,存储所有需要监控的文件事件。文件事件结构体定义如下:

typedef struct aeFileEvent {
    int mask; //存储监控的文件事件类型 /* one of AE_(READABLE|WRITABLE|BARRIER) */
    aeFileProc *rfileProc; //函数指针,指向读事件处理函数
    aeFileProc *wfileProc; //函数指针,指向写事件处理函数
    void *clientData; //指向对应的客户端对象
} aeFileEvent;

mask存储监控的文件事件类型,如AE_READABLE可读事件和AE_WRITABLE可写事件。
rfileProc 函数指针,指向读事件处理函数。
wfileProc 函数指针,指向写事件处理函数。
clientData 指向对应的客户端对象。

  • 调用aeApiAddEvent函数添加事件之前,首先需要调用aeCreateFileEvent函数创建对应的文件事件,并存储在aeEventLoop结构体的events字段,aeCreateFileEvent函数实现如下:
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    aeFileEvent *fe = &eventLoop->events[fd];//提取指针

    if (aeApiAddEvent(eventLoop, fd, mask) == -1)
        return AE_ERR;
    fe->mask |= mask;//判断独写类型标志
    if (mask & AE_READABLE) fe->rfileProc = proc;//添加读函数
    if (mask & AE_WRITABLE) fe->wfileProc = proc;//添加写函数
    fe->clientData = clientData;//添加对应的客户端对象
    if (fd > eventLoop->maxfd) //判断是否需要更改最大的文件描述符
        eventLoop->maxfd = fd;
    return AE_OK;
}

Redis服务器启动时需要创建socket并监听,等待客户端连接;客户端与服务器建立socket连接之后,服务器会等待客户端的命令请求;服务器处理完客户端的命令请求之后,命令回复会暂时存在client结构体的buf缓冲区中,待客户端文件描述符的可写事件发生时,才会真正往客户端发送命令回复。这些都需要创建对应的文件事件:

	//创建一个事件来监听socket并使用acceptTcpHandler函数来处理
    for (j = 0; j < server.ipfd_count; j++) {
        if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL) == AE_ERR)
            {
                serverPanic(
                    "Unrecoverable error creating server.ipfd file event.");
            }
    }
  • 从Redis 5.0之后的新版本开始,接受客户端请求的方法和响应客户端的方法,readQueryFromClientsendReplyToClient分别在acceptTcpHandlerhandleClientsWithPendingReadsUsingThreads 中(handleClientsWithPendingReadsUsingThreadsafterSleep中有调用),同时Redis也开始采用多线程的方式去响应客户端的请求,提高吞吐量。

  • 这里同时引出一个问题,aeApiPoll函数的第二个参数是时间结构体timeval,存储调用epoll_wait时传入的超时时间,那么这个时间是怎么计算出来的呢?前面我们说过,Redis需要处理各种文件事件,同时还需要处理很多定时任务(时间事件),那么当Redis由于执行epoll_wait而阻塞时,恰巧定时任务到期需要处理的时候怎么办?答案是:Redis会通过循环执行函数aeProcessEvents,在调用aeApiPoll之前遍历Redis时间事件的链表,查找最终发生的时间事件,以此作为aeApiPoll需要传入的超时时间。代码如下(aeProcessEvents函数实现):

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
    
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;

        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            shortest = aeSearchNearestTimer(eventLoop); //查找最近发生的事件
        if (shortest) {
            long now_sec, now_ms;
            //获取当前时间
            aeGetTime(&now_sec, &now_ms);
            tvp = &tv;
        
            long long ms =
                (shortest->when_sec - now_sec)*1000 +
                shortest->when_ms - now_ms;

            if (ms > 0) {
                tvp->tv_sec = ms/1000;
                tvp->tv_usec = (ms % 1000)*1000;
            } else {
                tvp->tv_sec = 0;
                tvp->tv_usec = 0;
            }
        } else {
        
            //不需要等待,设置时间等待为0
            if (flags & AE_DONT_WAIT) {
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                tvp = NULL; /* wait forever */
            }
        }

        if (eventLoop->flags & AE_DONT_WAIT) {
            tv.tv_sec = tv.tv_usec = 0;
            tvp = &tv;
        }
    
        //阻塞等待文件事件发生
        numevents = aeApiPoll(eventLoop, tvp);
       
        if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
            eventLoop->aftersleep(eventLoop);

		//执行已经触发的文件事件
        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 fired = 0; /* Number of events fired for current fd. */
            //处理文件事件,并且根据类型去执行 读函数或者写函数           
            //判断是否为等待一同处理类型的事件?
            int invert = fe->mask & AE_BARRIER;        
            //被触发的读事件
            if (!invert && fe->mask & mask & AE_READABLE) {
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                fired++;
            }          
            //没有被触发的写事件
            if (fe->mask & mask & AE_WRITABLE) {
                if (!fired || fe->wfileProc != fe->rfileProc) {
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }
            //没有被触发的读事件
            if (invert && fe->mask & mask & AE_READABLE) {
                if (!fired || fe->wfileProc != fe->rfileProc) {
                    fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }
            processed++;
        }
    }

4、时间事件

  • 前面介绍了Redis文件事件,已经知道事件循环执行函数aeProcessEvents,时间事件的触发和文件事件的触发都在该函数中,其主要逻辑如下:

1、查找最早会发生的时间事件,计算超时时间
2、阻塞等待文件事件的产生
3、处理文件事件
4、处理时间事件,事件事件的处理函数为processTimeEvents;

  • Redis服务器内部有很多定时任务需要执行,比如定时任务清楚超时客户端连接,定时任务删除过去键等等,定时任务被封装为时间时间aeTimeEvent对象,多个时间事件形成链表,存储在aeEventLoop结构体的timeEventHead字段,它指向链表首节点。时间事件aeTimeEvent结构体定义如下:
typedef struct aeTimeEvent {
    long long id; //时间事件ID/* time event identifier. */
    long when_sec; //触发的秒数/* seconds */
    long when_ms;  //触发的毫秒数/* milliseconds */
    aeTimeProc *timeProc; //函数指针,指向时间事件的处理函数
    aeEventFinalizerProc *finalizerProc; //函数指针,删除时间事件节点之前会先调用该函数(就是个钩子)
    void *clientData;//指向对应客户端对象
    struct aeTimeEvent *prev;//上一个节点
    struct aeTimeEvent *next;//下一个节点
} aeTimeEvent;

各字段含义如下:
id 时间事件唯一ID,通过字段eventLoop->timeEventNextId实现;
when_sec 和 when_ms 事件事件触发的秒数与毫秒数;
timeProc 函数指针,指向时间事件处理函数;
finalizerProc 函数指针,删除时间事件节点之前会调用此函数;
clientData 指向需要使用的对应客户端数据;
prev 和 next 指向上一个或下一个事件事件链表节点;

  • 时间事件执行函数processTimeEvents的处理逻辑比较简单,只是遍历时间事件链表,判断当前时间事件是否已经到期,如果到期执行时间事件处理函数timeProc如下:
static int processTimeEvents(aeEventLoop *eventLoop) {
    int processed = 0;
    aeTimeEvent *te;
    long long maxId;
    time_t now = time(NULL);

    //防止ntp 时间同步带来的时间回拨,这样通过循环设置当前已经触发的事件等待时间为0
    if (now < eventLoop->lastTime) {
        te = eventLoop->timeEventHead;
        while(te) {
            te->when_sec = 0;
            te = te->next;
        }
    }
    eventLoop->lastTime = now;

    te = eventLoop->timeEventHead;
    maxId = eventLoop->timeEventNextId-1;
    while(te) {
        long now_sec, now_ms;
        long long id;
    
        //判断该事件是否已需要删除
        if (te->id == AE_DELETED_EVENT_ID) {
            aeTimeEvent *next = te->next;
            if (te->prev)
                te->prev->next = te->next;
            else
                eventLoop->timeEventHead = te->next;
            if (te->next)
                te->next->prev = te->prev;
            if (te->finalizerProc)
                te->finalizerProc(eventLoop, te->clientData);
            zfree(te);
            te = next;
            continue;
        }     
        //一个防御校验,防止当前事件的ID已经溢出,正常的事件ID是递增的,防止认为导致添加异常事件
        if (te->id > maxId) {
            te = te->next;
            continue;
        }
        //获取系统事件
        aeGetTime(&now_sec, &now_ms);
        //判断当前事件是否到期,到期执行
        if (now_sec > te->when_sec ||
            (now_sec == te->when_sec && now_ms >= te->when_ms))
        {
            int retval;

            id = te->id;
            //执行该事件事件 retval 是下次触发该事件的时间
            retval = te->timeProc(eventLoop, id, te->clientData);
            processed++;
            //设置时间事件的到期时间
            if (retval != AE_NOMORE) {
                aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
            } else {
                te->id = AE_DELETED_EVENT_ID;
            }
        }
        te = te->next;
    }
    return processed;
}
  • 事件函数timeProc的返回值retval,表示该事件下次被触发的事件,单位为秒,并且是一个相对时间(因为任务可能执行的比久),即从当前时间算起,retval毫秒后此时间事件会被触发。
  • 其实Redis只有一个时间事件,和文件事件一样通过aeTimeEvent去封装时间事件,再通过aeCreateTimeEvent去创建相应的时间事件,函数定义如下:
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc);

各个字段的含义如下:
eventLoop 输入参数指向事件循环结构体;
milliseconds 表示此时间事件触发时间,单位毫秒,注意这里是一个相对时间,即从当前时间开始算起,milliseconds毫秒后会被触发;
proc 指向时间事件的处理函数;
clientData 指向对应的结构体对象;
finalizerProc 函数指针,删除事件的时候调用,相当于钩子;

  • 通过全局搜索,会发现只有一个地方调用了(server.c文件,其它的是非业务主线功能,比如集群同步相关的等等)aeCreateTimeEvent,只创建了一个时间事件,代码如下:
	/* Create the timer callback, this is our way to process many background
     * operations incrementally, like clients timeout, eviction of unaccessed
     * expired keys and so forth. */
    if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        serverPanic("Can't create event loop timers.");
        exit(1);
    }

该时间事件在1毫秒后触发,处理函数为serverCron,参数clientDatafinalizerProc都为null,而函数serverCron实现了Redis服务所有的定时任务,进行周期指向,代码如下:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
.......................................
 run_with_period(100) {
 //100毫秒周期执行
 }
 run_with_period(500) {
 //500毫秒周期执行
 }

 //清除超时客户端连接
 clientsCron();
 //处理数据库
 databasesCron();

 server.cronloops++;
 return 1000/server.hz;
}
  • 变量 server.cronloops用于记录serverCron函数的执行次数,变量server.hz表示serverCron函数的执行频率,用户可以配置,最小为1最大为500,默认为10。假设server.hz取默认值10,函数返回1000/server.hz,会更新当前时间事件的触发时间为100毫秒,即serverCron的执行周期为100毫秒。run_with_period宏定义实现了定时任务按照指定时间周期(_ ms _)执行,此时会被替换为一个if条件判断,条件为真才会执行定时任务,定义如下:
#define run_with_period(_ms_) if ((_ms_ <= 1000/server.hz) || !(server.cronloops%((_ms_)/(1000/server.hz))))
  • 另外可以看到,serverCron函数会无条件执行某些定时任务,比如清除超时客户端连接,以及处理数据库等等(清除过期键等)。需要特别注意一点,serverCron函数的执行时间不能过长,否则会导致服务器不能及时响应客户端的请求。下面以过期键删除为例子,分析Redis是如何保证serverCron函数的执行时间。过期键删除由函数activeExpireCycle实现,由函数databasesCron调用,其函数实现如下:
void activeExpireCycle(int type) {
	timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
	static int timelimit_exit = 0;  
	............................................
	for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        unsigned long expired, sampled;

        redisDb *db = server.db+(current_db % server.dbnum);       
        current_db++;

      
        do {
        .......................................
            //查找并删除过期键
            if ((iteration & 0xf) == 0) { 
                elapsed = ustime()-start;
                //检测是否超时,超时要提前结束
                if (elapsed > timelimit) {
                    timelimit_exit = 1;
                    server.stat_expired_time_cap_reached_count++;
                    break;
                }
            }
           
        } while ((expired*100/sampled) > config_cycle_acceptable_stale);//判断是否超时
    }
}
  • 函数activeExpireCycle最多遍历dbs_per_call个数据库,并记录每个数据库删除的过期键数目;当删除过期键数目大于最大限制时候,认为数据库过期键比较多,需要再次处理。考虑到极端情况,当数据库键数目非常多且基本都过期的时候,do-while循环会一直执行下去。因此我们添加timelimit时间限制,每执行16次do-while循环,检测函数activeExpireCycle执行时间是否超过timelimit,如果超过直接结束循环。
  • timelimit 的计算方式也是有讲究的,timelimit计算出来的值要求让activeExpireCycle函数的总执行时间占CPU的25%,即每秒函数activeExpireCycle的总执行时间为 1000000*25/100微秒。仍然假设server.hz默认值取10,即每秒函数activeExpireCycle执行10次,那么每次函数调用的时间最多只有 1000000*25/100/10单位微秒。

5、总结

  • 说到这里,Redis的整个指令运行周期也讲的差不多了,Redis是一个事件驱动的服务模型,根据上面的内容,我们其实大概可以总结出这样的一个图:
    在这里插入图片描述
    Redis的事件还多更加深入的实现,也欢迎大家继续阅读源码,去挖掘Redis的内幕。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章