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的多线程】
Redis源码阅读【9-持久化】
Redis源码阅读【10-事务与Lua】
建议搭配源码阅读:源码地址
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
为事件处理主函数,其中第二个参数是一个标志flags
,AE_ALL_EVENTS
标识函数需要处理文件事件与时间事件,AE_CALL_AFTER_SLEEP
表示阻塞等待文件事件需要执行aftersleep
函数。
3、文件事件
- Redis客户端通过TCP socket 与服务器端进行交互,文件事件指的就是socket的可读可写事件。socket独写操作有阻塞和非阻塞之分。采用的是阻塞模式时,一个进程只能处理一条网络连接的独写请求,为了同时处理多条网络连接,通常会采用多线程或者多进程的方式;非阻塞模式下,可以使用目前比较成熟的I/O多路复用模型,比如 select/epoll/kqueue等。视不同的操作系统而定。
- 基于Linux系统,主要还是使用epoll。epoll是Linux内核为处理大量并发网络连接而提出的解决方案,能显著提升操作系统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.c
,ae_epoll.c
,ae_kqueue.c
和ae_select.c
实现。 -
而Redis在编译的时候会检查操作系统支持的I/O多路复模型来选择使用哪种。
-
以epoll为例
aeApiCreate
函数是对epoll_create
的封装;aeApiAddEvent
函数用于添加事件,是对epoll_ctl
的封装;aeApiDelEvent
函数用于删除事件,是对epoll_ctl
的封装;aeApiPoll
是对epoll_wait
的封装。此外还有aeApiResize
和aeApiFree
,是综合封装用于扩展和完全释放使用。函数定义如下:
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.c
和ae_epoll.c
中都实现了static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
这个方法。这种方式可以类比java里面的接口,通过这种方式,我们可以无缝的切换各种类型的I/O多路复用模型:
-
上面简单介绍了epoll的使用,以及Redis对epoll等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之后的新版本开始,接受客户端请求的方法和响应客户端的方法,
readQueryFromClient
和sendReplyToClient
分别在acceptTcpHandler
和handleClientsWithPendingReadsUsingThreads
中(handleClientsWithPendingReadsUsingThreads
在afterSleep
中有调用),同时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
,参数clientData
和finalizerProc
都为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的内幕。