深入浅出理解单机Redis核心工作流程

RedisServer 单机核心流程源码分析

主要理解一下核心流程,其中删掉了和本次分析不挂钩的相关代码,同时只是分析单机版Redis而并不是集群版本.

一、Redis Server 核心流程分析

redisServer 启动入口在 src/redis.c 中的main入口启动

c 复制代码
int main(int argc, char **argv) {
    struct timeval tv;
    printf("zhenxinma helloword\n");
    /*
     * 初始化服务器
     * 1、初始化基础信息:Port等
     * 2、初始化主从复制相关参数
     * 3、创建命令参数字典
     *
     * */
    initServerConfig();
    // 创建并初始化服务器数据结构
    // initServer 启动TCP 监听Client命令到来
    initServer();
    // 如果服务器是守护进程,那么创建 PID 文件
    if (server.daemonize) createPidFile();
    // 为服务器进程设置名字
    redisSetProcTitle(argv[0]);
    // 打印 ASCII LOGO
    redisAsciiArt();
    // 运行事件处理器,一直到服务器关闭为止
    // 什么时机调用的呢?
    aeSetBeforeSleepProc(server.el,beforeSleep);
    aeMain(server.el);
    // 服务器关闭,停止事件循环
    aeDeleteEventLoop(server.el);
    return 0;
}

精简下来的单机Redis源码入口就是很简单,一共分为三步:

  1. 初始化redisServer配置,包括从redis.conf中读取或者走默认值,这一步就是简单给redisServer赋值默认值,这一步省略掉
  2. 初始化redisServer网络相关配置,比如监听TCP链接,创建时间事件以及文件事件等,同时创建eventLoop结构并赋值为全局redisServer的el变量,用来全局引用事件变量
  3. aeMain事件循环Loop,不断检查是否有就绪的文件事件以及事件时间的到达,如果有则执行对应的协议函数,如果没有则执行下次循环

我们先通过下面这张图来先简单了解写Redis核心流程

通过上面的图片我们可以简单理解单机RedisServer的事件处理流程,下面就分别详细介绍文件事件、时间事件以及主要的工作流程

这里我们带着下面几个问题来去理解Redis工作流程:

  • RedisServer是如何接受TCP连接来创建Client的?
  • RedisServer是如何获取client发送来的数据的?
  • RedisServer是如何向Client发送数据的?
  • RedisServerDB存储结构

二、Redis 事件的概念

文件事件

下面是Redis中的核心aeEventLoop结构体,后续Redis触发文件事件以及时间事件都是通过该结构体来触发的

c 复制代码
typedef struct aeEventLoop {
    // 目前已注册的最大描述符
    int maxfd;   /* highest file descriptor currently registered */
    // 目前已追踪的最大描述符
    int setsize; /* max number of file descriptors tracked */
    // 用于生成时间事件 id
    long long timeEventNextId;
    // 最后一次执行时间事件的时间
    time_t lastTime;     /* Used to detect system clock skew */
    // 已注册的文件事件
    aeFileEvent *events; /* Registered events */
    // 已就绪的文件事件
    aeFiredEvent *fired; /* Fired events */
    // 时间事件
    aeTimeEvent *timeEventHead;
    // 事件处理器的开关
    int stop;
    // 多路复用库的私有数据 封装epoll的结构体
    void *apidata; /* This is used for polling API specific data */
    // 在处理事件前要执行的函数
    aeBeforeSleepProc *beforesleep;

} aeEventLoop;

其中有关文件事件的有有三个字段,分别如下:

  1. aeFileEvent *events Redis启动后注册的文件事件
  2. aeFiredEvent *fired Redis启动后通过Epool Wait回调将已经就绪的文件事件存放在这里
  3. void *apidataRedis封装的Epoll库,Redis操作Epoll实际上都是通过aeApiState来进行操作的,其具体的结构体如下
c 复制代码
typedef struct aeApiState {
    // epoll_event 实例描述符
    int epfd;
    // 已注册的事件
    struct epoll_event *events;
} aeApiState;

struct epoll_event
{
  uint32_t events;	/* 文件句柄 */
  epoll_data_t data;	/* 用户数据/read or write */
} __EPOLL_PACKED;

其中epfd为epoll的文件句柄,events为已经注册的文件事件,大概的流程为,当Epoll上有事件到达时,Redis会通过epoll_wait阻塞等待即将到来的时间文件事件,后续通过该文件事件去调用之前已经注册的回调函数,根据MASK掩码来根据不同的场景调用不同的回调函数

时间事件

Redis启动时会在initServer中注册一个时间事件:

c 复制代码
// redis.c/ 2161 line
if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
   redisPanic("Can't create the serverCron time event.");
   exit(1);
}

// ae.c/ 283 line
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc)
{
    // 更新时间计数器
    long long id = eventLoop->timeEventNextId++;
    // 创建时间事件结构
    aeTimeEvent *te;
    te = zmalloc(sizeof(*te));
    if (te == NULL) return AE_ERR;
    // 设置 ID
    te->id = id;
    // 设定处理事件的时间
    aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
    // 设置事件处理器
    te->timeProc = proc;
    te->finalizerProc = finalizerProc;
    // 设置私有数据
    te->clientData = clientData;
    // 将新事件放入表头
    te->next = eventLoop->timeEventHead;
    eventLoop->timeEventHead = te;
    return id;
}

我们可以看到,其会给RedisServer.enventloop.timeEventHead赋值为当前创建的时间事件,其事件发生的回调函数为serverCron,同时设置的为每一毫秒调用一次,具体调用时机在单线程aeMain中进行调用,并且每次调用完serverCron之后,会重新添加一个相同的时间事件,具体时间事件有做什么逻辑后面再进行详细分析

Redis创建EventLoop:

c 复制代码
// redis.c/ 2090 line
server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);

// ae.c/ 66 line
aeEventLoop *aeCreateEventLoop(int setsize) {
    aeEventLoop *eventLoop;
    int i;
    // 创建事件状态结构
    if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;
    // 初始化文件事件结构和已就绪文件事件结构数组
    eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
    eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
    if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;
    // 设置数组大小
    eventLoop->setsize = setsize;
    // 初始化执行最近一次执行时间
    eventLoop->lastTime = time(NULL);
    // 初始化时间事件结构
    eventLoop->timeEventHead = NULL;
    eventLoop->timeEventNextId = 0;
    eventLoop->stop = 0;
    eventLoop->maxfd = -1;
    eventLoop->beforesleep = NULL;
    // 创建Epoll
    if (aeApiCreate(eventLoop) == -1) goto err;
    /* Events with mask == AE_NONE are not set. So let's initialize the
     * vector with it. */
    // 初始化监听事件
    for (i = 0; i < setsize; i++)
        eventLoop->events[i].mask = AE_NONE;
    // 返回事件循环
    return eventLoop;

err:
    if (eventLoop) {
        zfree(eventLoop->events);
        zfree(eventLoop->fired);
        zfree(eventLoop);
    }
    return NULL;
}

// ae_epoll.c/50 line
// 其中会在aeApiCreate函数内部创建epoll实例
static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));
    if (!state) return -1;
    // 初始化事件槽空间
    state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
    if (!state->events) {
        zfree(state);
        return -1;
    }
    // 创建 epoll 实例
    state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
    if (state->epfd == -1) {
        zfree(state->events);
        zfree(state);
        return -1;
    }
    // 赋值给 eventLoop
    eventLoop->apidata = state;
    return 0;
}

我们看到,Redis在初始化EventLoop的时候会进行epoll_create调用来创建epoll实例,最后将epoll的文件句柄赋值给aeApiState.epfd,后续可以通过该文件句柄来和epoll进行交互.

不理解Epoll的可以看这篇文章: openxm.cn/#/article/i...

三、Redis 初始化TCP Listener.

Redis初始化TCP Listener是在Redis启动的时候创建的第一个文件事件,这里创建文件事件的含义为接受TCP链接,用来创建客户端,具体逻辑如下:

首先Redis会针对指定配置创建TCP Listener,下面几行代码将会创建TCP Listener用来监听客户端连接

c 复制代码
if (server.port != 0 &&
    listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR)
    exit(1);

上面打开TCP Listener之后会将对应的文件句柄fd交给epoll来管理,具体看下面代码:

c 复制代码
// redis.c/ 2171 line
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
    acceptTcpHandler,NULL) == AE_ERR)
    {
        redisPanic(
            "Unrecoverable error creating server.ipfd file event.");
    }

// ae.c/ 170 line
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    if (fd >= eventLoop->setsize) return AE_ERR;
    // 取出文件事件结构
    aeFileEvent *fe = &eventLoop->events[fd];
    // 监听指定 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;
    // 如果有需要,更新事件处理器的最大 fd
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd;
    return AE_OK;
}

我们可以看到,上面aeCreateFileEvent中会将给定的文件句柄交给epoll来管理,也就是说会将文件句柄通过epoll_ctl来将当前的文件句柄添加到EventLoop.apidata.epfd所管理的epoll实例中。

到这里,Redis理论上就可以接受客户端的TCP链接了,具体在哪里接受我们往下看。Redis在初始化完之后会启动一个aeMain循环机制,来不断地轮训当前EventLoop中是否有新的事件可以执行,同事也会在这里调用一次epoll_wait来进行循环等待文件事件的到来,具体如下:

c 复制代码
// redis.c/4110 line
// main入口中,初始化Redis的各个配置以及各个事件之后,就会执行这个aeMain
aeMain(server.el);

// ae.c/630 line
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        // 如果有需要在事件处理前执行的函数,那么运行它
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        // 开始处理事件
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

// ae.c/510 line
// 这里会精简一些代码,后面会在给一个伪代码的形式来帮助理解
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    	// tvp计算时间为下一次时间事件发生时距离现在的秒数或者毫秒数
    	tvp;
        // 处理文件事件,阻塞时间由 tvp 决定
    	// 执行epoll_wait等待,直到有事件到来
        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;
                // networking.c: 1548 readQueryFromClient
                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++;
        }
    }
    // 执行时间事件
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);

    return processed; /* return the number of processed file/time events */
}

我们再来简单看一下aeApiPoll是如何调用epoll_wait

c 复制代码
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    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);
    // 在timeout内如果没有事件发生则直接return 0
    if (retval > 0) {
        int j;
        // 为已就绪事件设置相应的模式
        // 并加入到 eventLoop 的 fired 数组中
        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            //  初始化epoll_event的掩码`mask`后续会通过mask来决定调用什么类型的回调函数
            struct epoll_event *e = state->events+j;
            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    
    // 返回已就绪事件个数
    return numevents;
}

从上面我们可以看到epoll_wait会返回已经就绪的文件事件,同时初始化各个事件的掩码MASK用来后续决定调用什么回调函数来处理本次事件,OK,我们在次回头看一下aeProcessEvents处理函数内,其会通过aeFileEvent.mask来决定调用什么类型的回调函数,而不同的回调函数我们已经在各个地方进行处理了,

  • Listener 回调函数 acceptTcpHandler
  • 客户端Read函数 readQueryFromClient
  • 客户端Write函数 sendReplyToClient

下面针对每一种场景分别来简单理解一下:

3.1 Listener 回调函数 acceptTcpHandler

通过上面分析我们可以知道,当有一个客户端连接上来的时候会通过epoll_wait告知我们当前的Listener状态的文件句柄上有可读事件发生,此时在aeMain中将会执行acceptTcpHandler回调函数

c 复制代码
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[REDIS_IP_STR_LEN];
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);
    REDIS_NOTUSED(privdata);

    while(max--) {
        // anetTcpAccept 客户端的TCP conn
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                redisLog(REDIS_WARNING,
                    "Accepting client connection: %s", server.neterr);
            return;
        }
        redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
        // 为客户端创建客户端状态(redisClient),同时添加针对该TCP Conn添加read回调函数
        acceptCommonHandler(cfd,0);
    }
}

上面的代码我们可以理解到,当Listener状态连接有数据到来时将会执行回调函数acceptTcpHandler,而该回调函数内部会针对每一个tcp conn创建一个redisClietn用来表示当前连接的客户端,具体代码如下:

c 复制代码
static void acceptCommonHandler(int fd, int flags) {
    // 创建客户端
    redisClient *c;
    if ((c = createClient(fd)) == NULL) {
        redisLog(REDIS_WARNING,
            "Error registering fd event for the new client: %s (fd=%d)",
            strerror(errno),fd);
        close(fd); /* May be already closed, just ignore errors */
        return;
    }
    // 如果新添加的客户端令服务器的最大客户端数量达到了
    // 那么向新客户端写入错误信息,并关闭新客户端
    // 先创建客户端,再进行数量检查是为了方便地进行错误信息写入
    if (listLength(server.clients) > server.maxclients) {
        char *err = "-ERR max number of clients reached\r\n";

        /* That's a best effort error message, don't check write errors */
        if (write(c->fd,err,strlen(err)) == -1) {
            /* Nothing to do, Just to avoid the warning... */
        }
        // 更新拒绝连接数
        server.stat_rejected_conn++;
        freeClient(c);
        return;
    }

    // 更新连接次数
    server.stat_numconnections++;

    // 设置 FLAG
    c->flags |= flags;
}


redisClient *createClient(int fd) {

    // 分配空间
    redisClient *c = zmalloc(sizeof(redisClient));

    /* passing -1 as fd it is possible to create a non connected client.
     * This is useful since all the Redis commands needs to be executed
     * in the context of a client. When commands are executed in other
     * contexts (for instance a Lua script) we need a non connected client. */
    // 当 fd 不为 -1 时,创建带网络连接的客户端
    // 如果 fd 为 -1 ,那么创建无网络连接的伪客户端
    // 因为 Redis 的命令必须在客户端的上下文中使用,所以在执行 Lua 环境中的命令时
    // 需要用到这种伪终端
    if (fd != -1) {
        // 非阻塞
        anetNonBlock(NULL,fd);
        // 禁用 Nagle 算法
        anetEnableTcpNoDelay(NULL,fd);
        // 设置 keep alive
        if (server.tcpkeepalive)
            anetKeepAlive(NULL,fd,server.tcpkeepalive);
        // 绑定读事件到事件 loop (开始接收命令请求)
        if (aeCreateFileEvent(server.el,fd,AE_READABLE,
            readQueryFromClient, c) == AE_ERR)
        {
            close(fd);
            zfree(c);
            return NULL;
        }
    }

    // 初始化各个属性,这里都是针对字段来进行初始化,这里就不在一一赘述
    // 默认数据库
    selectDb(c,0);
    // 套接字
    c->fd = fd;
   	// ...... 初始化其他各个字段
    // 返回客户端
    return c;
}

通过函数createClient我们现在针对每一个tcp conn都创建了一个redisClient,并且同时将当前客户端的文件句柄也添加到了epoll中,同时为当前event添加了readQueryFromClient回调函数用来当客户端向redis发送数据时通过该函数来进行接受数据

3.2 读取客户端数据回调函数 readQueryFromClient

这种场景下其实就简单了很多,因为上面已经分析的足够简单了,当epoll上有可读事件触发时会通过不同的掩码MASK来触发不同的回调函数,如果是普通的TCP客户端连接,此时通过掩码AE_READABLE就可以直接调用该事件注册的回调函数readQueryFromClient,而在该函数内部总共就做了三件事情:

  1. 解析客户端发来的命令RESP
  2. 通过commondTable找到本次对应的命令,然后执行保存数据或者更新数据
  3. 响应客户端,在响应客户端哪里会增加一个EventLoopWrite事件,当事件触发则向Client写入数据

这里的源码就不贴了,本文主要是分析单机redisServer的工作流程,而这里其实就是按照上面的步骤一步一步的进行,解析命令然后执行,最后响应。

3.3 向客户端写数据回调函数 sendReplyToClient

通过上面的分析,我们可以知道,RedisServerreadQueryFromClient回调函数中相应客户端时会增加一个文件事件,同时设置事件的回调函数为sendReplyToClient,当有该事件发生时,则会直接调用,在该函数内部会直接通过客户端的文件句柄fd来直接进行write

c 复制代码
nwritten = write(fd,c->buf+c->sentlen,c->bufpos-c->sentlen);

OK,到这里其实Redis整个基本的工作原理就理解了,后续Redis常用的数据结构以及持久化和分布式能力在后续进行文章中进行发布。

四、名词解释

名词解释放到最后

Linux文件:/proc/sys/vm/overcommit_memory表示的含义为Linux 内存分配策略,有以下三种情况:

  • 0:表示内核将检查是否有足够的可用内存供应用进程使用,如果有足够的可用内存,则内存申请允许,否则内存申请失败,并返回错误给应用程序
  • 1:内核允许分配所有物理内存,而不管当前的内存状态如何
  • 2:表示内核允许分配超过所有物理内存和交换空间总和内存
相关推荐
后端小张1 小时前
Redis 执行 Lua,能保证原子性吗?
数据库·redis·缓存
lipviolet1 小时前
Redis系列---Redission分布式锁
数据库·redis·分布式
A_cot14 小时前
Redis 的三个并发问题及解决方案(面试题)
java·开发语言·数据库·redis·mybatis
芊言芊语16 小时前
分布式缓存服务Redis版解析与配置方式
redis·分布式·缓存
攻城狮的梦17 小时前
redis集群模式连接
数据库·redis·缓存
Amagi.20 小时前
Redis的内存淘汰策略
数据库·redis·mybatis
无休居士20 小时前
【实践】应用访问Redis突然超时怎么处理?
数据库·redis·缓存
.Net Core 爱好者21 小时前
Redis实践之缓存:设置缓存过期策略
java·redis·缓存·c#·.net
码爸1 天前
flink 批量压缩redis集群 sink
大数据·redis·flink
微刻时光1 天前
Redis集群知识及实战
数据库·redis·笔记·学习·程序人生·缓存