分类 Redis 下的文章

在 C/C++ 很大一部分教材中都说不要使用 goto 关键字进行逻辑跳转,这是因为 goto 容易造成代码的不可维护,因为太多的跳转容易导致代码逻辑的凌乱,让人摸不到头脑,但是有一种情况是 goto 的不错应用场景,那就是在应用初始化失败时候来清理资源。

在《Linux C 程序设计王者归来》一书中有详细的解释说明。

接下来我们来看看在 redis 中的用法,其源代码在 src/anet.c 第 330 行:

            if (!bound) {
                anetSetError(err, "bind: %s", strerror(errno));
                goto error;
            }

如果 bound 失败,则直接跳转到 error 代码块,而 error 代码块就是处理资源释放的。


error:
    if (s != ANET_ERR) {
        close(s);
        s = ANET_ERR;
    }

redis-client 提供了类似于与 mysql 的交互式命令行工具,我们输入命令,redis-client 会将我们的命令发送给 redis-server 然后返回取到的值。

linenoise 是非常轻量级的命令行处理工具,除了 redis 还有很著名的开源软件在使用 linenoise 其中就包括 MongoDB 和 Android 都将 Linenoise 作为命令行解析工具,那么今天我们就来解锁这个开源的命令行处理工具,也许某一天在你的项目里会派上用场。

其使用方法我们可以参考这篇文章:https://zhuanlan.zhihu.com/p/103845625

其源代码被 redis 放在 deps 目录下。

redis 对齐的调用封装在 src/redis-cli.c 中的 repl 函数中.

static void repl(void) {
    sds historyfile = NULL;
    int history = 0;
    char *line;
    int argc;
    sds *argv;

    /* There is no need to initialize redis HELP when we are in lua debugger mode.
     * It has its own HELP and commands (COMMAND or COMMAND DOCS will fail and got nothing).
     * We will initialize the redis HELP after the Lua debugging session ended.*/
    if (!config.eval_ldb) {
        /* Initialize the help using the results of the COMMAND command. */
        cliInitHelp();
    }

    config.interactive = 1;
    linenoiseSetMultiLine(1);
    linenoiseSetCompletionCallback(completionCallback);
    linenoiseSetHintsCallback(hintsCallback);
    linenoiseSetFreeHintsCallback(freeHintsCallback);

    /* Only use history and load the rc file when stdin is a tty. */
    if (isatty(fileno(stdin))) {
        historyfile = getDotfilePath(REDIS_CLI_HISTFILE_ENV,REDIS_CLI_HISTFILE_DEFAULT);
        //keep in-memory history always regardless if history file can be determined
        history = 1;
        if (historyfile != NULL) {
            linenoiseHistoryLoad(historyfile);
        }
        cliLoadPreferences();
    }

    cliRefreshPrompt();
    while((line = linenoise(context ? config.prompt : "not connected> ")) != NULL) {
        if (line[0] != '\0') {
            long repeat = 1;
            int skipargs = 0;
            char *endptr = NULL;

            argv = cliSplitArgs(line,&argc);
            if (argv == NULL) {
                printf("Invalid argument(s)\n");
                fflush(stdout);
                if (history) linenoiseHistoryAdd(line);
                if (historyfile) linenoiseHistorySave(historyfile);
                linenoiseFree(line);
                continue;
            } else if (argc == 0) {
                sdsfreesplitres(argv,argc);
                linenoiseFree(line);
                continue;
            }

            /* check if we have a repeat command option and
             * need to skip the first arg */
            errno = 0;
            repeat = strtol(argv[0], &endptr, 10);
            if (argc > 1 && *endptr == '\0') {
                if (errno == ERANGE || errno == EINVAL || repeat <= 0) {
                    fputs("Invalid redis-cli repeat command option value.\n", stdout);
                    sdsfreesplitres(argv, argc);
                    linenoiseFree(line);
                    continue;
                }
                skipargs = 1;
            } else {
                repeat = 1;
            }

            if (!isSensitiveCommand(argc - skipargs, argv + skipargs)) {
                if (history) linenoiseHistoryAdd(line);
                if (historyfile) linenoiseHistorySave(historyfile);
            }

            if (strcasecmp(argv[0],"quit") == 0 ||
                strcasecmp(argv[0],"exit") == 0)
            {
                exit(0);
            } else if (argv[0][0] == ':') {
                cliSetPreferences(argv,argc,1);
                sdsfreesplitres(argv,argc);
                linenoiseFree(line);
                continue;
            } else if (strcasecmp(argv[0],"restart") == 0) {
                if (config.eval) {
                    config.eval_ldb = 1;
                    config.output = OUTPUT_RAW;
                    sdsfreesplitres(argv,argc);
                    linenoiseFree(line);
                    return; /* Return to evalMode to restart the session. */
                } else {
                    printf("Use 'restart' only in Lua debugging mode.\n");
                    fflush(stdout);
                }
            } else if (argc == 3 && !strcasecmp(argv[0],"connect")) {
                sdsfree(config.conn_info.hostip);
                config.conn_info.hostip = sdsnew(argv[1]);
                config.conn_info.hostport = atoi(argv[2]);
                cliRefreshPrompt();
                cliConnect(CC_FORCE);
            } else if (argc == 1 && !strcasecmp(argv[0],"clear")) {
                linenoiseClearScreen();
            } else {
                long long start_time = mstime(), elapsed;

                issueCommandRepeat(argc-skipargs, argv+skipargs, repeat);

                /* If our debugging session ended, show the EVAL final
                    * reply. */
                if (config.eval_ldb_end) {
                    config.eval_ldb_end = 0;
                    cliReadReply(0);
                    printf("\n(Lua debugging session ended%s)\n\n",
                        config.eval_ldb_sync ? "" :
                        " -- dataset changes rolled back");
                    cliInitHelp();
                }

                elapsed = mstime()-start_time;
                if (elapsed >= 500 &&
                    config.output == OUTPUT_STANDARD)
                {
                    printf("(%.2fs)\n",(double)elapsed/1000);
                }
            }
            /* Free the argument vector */
            sdsfreesplitres(argv,argc);
        }
        /* linenoise() returns malloc-ed lines like readline() */
        linenoiseFree(line);
    }
    exit(0);
}

实现代码

Redis 的守护进程实现代码是在 src/server.c 的第 6094 行,其代码如下:

void daemonize(void) {
    int fd;

    if (fork() != 0) exit(0); /* parent exits */
    setsid(); /* create a new session */

    /* Every output goes to /dev/null. If Redis is daemonized but
     * the 'logfile' is set to 'stdout' in the configuration file
     * it will not log at all. */
    if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        if (fd > STDERR_FILENO) close(fd);
    }
}

redis 的守护进程很简单执行流程如下。

  1. 调用 fork 派生子进程
  2. 调用成功后退出父进程
  3. 设置新的 session_id
  4. 关闭标准输入、标准输出和标准错误
  5. 判断文件描述符如果大于标准错误,则直接关闭 fd

在每一个进程打开,都会默认打开三哥文件描述符。


#define     STDIN_FILENO    0    /* standard input file descriptor */
#define    STDOUT_FILENO    1    /* standard output file descriptor */
#define    STDERR_FILENO    2    /* standard error file descriptor */

## 调用位置
int background = server.daemonize && !server.supervised;
if (background) daemonize();

服务端结构体 redisServer  存储 Redis 服务器的所有信息,包括但不限于数据库、配置参数、命令表、监听端口与地址、客户端列表、若干统计信息、RDB与AOF持久化相关信息、主从复制相关信息、集群相关信息等。

这是一个全局变量,在 server.c 第 86 行被声明。

0.09x 是目前能够下载到的最古老的版本,在代码中有标注,这是一个预览版,所有代码加起来在 1W 多行,其中包括了文档,和一些辅助脚本,第三方案例,真正的 C 语言实现的代码仅有 5200 多行,这其中其实已经包含了很多 Redis 的核心逻辑,阅读起来的障碍也会小很多。

Reids 7 的C 代码行数已经超过了15W行,阅读起来的难度可想而知。

2023-03-14T09:01:22.png

我们先来看一下 redis 的目录结构

2023-03-14T09:01:37.png

这个时候的目录非常简洁,工程目录下直接存放了所有的源代码,以及几个目录。

  • client-libraries 存放了各个语言的 client sdk,我们直接可以利用其中的 sdk 进行与 redis-server 的交互。
  • doc 存放了 redis 的所有文档。
  • utils 是 redis 的工具包,使用 ruby 进行编写的。

这个时候的 Makefile 异常简洁,没有后面那么复杂。一共只有 62 行,只要拥有一定的 C 语言基础相信直接就能看明白。

# Redis Makefile
# Copyright (C) 2009 Salvatore Sanfilippo <antirez at gmail dot com>
# This file is released under the BSD license, see the COPYING file

DEBUG?= -g
CFLAGS?= -std=c99 -pedantic -O2 -Wall -W -DSDS_ABORT_ON_OOM
CCOPT= $(CFLAGS)

OBJ = adlist.o ae.o anet.o dict.o redis.o sds.o zmalloc.o lzf_c.o lzf_d.o
BENCHOBJ = ae.o anet.o benchmark.o sds.o adlist.o zmalloc.o
CLIOBJ = anet.o sds.o adlist.o redis-cli.o zmalloc.o

PRGNAME = redis-server
BENCHPRGNAME = redis-benchmark
CLIPRGNAME = redis-cli

all: redis-server redis-benchmark redis-cli

# Deps (use make dep to generate this)
adlist.o: adlist.c adlist.h
ae.o: ae.c ae.h
anet.o: anet.c anet.h
benchmark.o: benchmark.c ae.h anet.h sds.h adlist.h
dict.o: dict.c dict.h
redis-cli.o: redis-cli.c anet.h sds.h adlist.h
redis.o: redis.c ae.h sds.h anet.h dict.h adlist.h zmalloc.c zmalloc.h
sds.o: sds.c sds.h
sha1.o: sha1.c sha1.h
zmalloc.o: zmalloc.c

redis-server: $(OBJ)
    $(CC) -o $(PRGNAME) $(CCOPT) $(DEBUG) $(OBJ)
    @echo ""
    @echo "Hint: To run the test-redis.tcl script is a good idea."
    @echo "Launch the redis server with ./redis-server, then in another"
    @echo "terminal window enter this directory and run 'make test'."
    @echo ""

redis-benchmark: $(BENCHOBJ)
    $(CC) -o $(BENCHPRGNAME) $(CCOPT) $(DEBUG) $(BENCHOBJ)

redis-cli: $(CLIOBJ)
    $(CC) -o $(CLIPRGNAME) $(CCOPT) $(DEBUG) $(CLIOBJ)

.c.o:
    $(CC) -c $(CCOPT) $(DEBUG) $(COMPILE_TIME) $<

clean:
    rm -rf $(PRGNAME) $(BENCHPRGNAME) $(CLIPRGNAME) *.o

dep:
    $(CC) -MM *.c

test:
    tclsh test-redis.tcl

bench:
    ./redis-benchmark

log:
    git log '--pretty=format:%ad %s' --date=short > Changelog

Redis 的入口

我们直接到 redis 的入口函数,其代码在 redis.c 中,直接拖拽滚动框到最底部即可,我对代码的每个阶段做了注释。启动一个 redis 服务其实 3 个阶段总计 8 个步骤。

  • 阶段一:基本初始化

    1. 初始化 server 配置,initServerConfig()。
    2. 判断是否有指定配置文件,如果有则重新加载配置文件,loadServerConfig()。
    3. 初始化服务,initServer()。
    4. 判断是否为后台执行,daemonize()。
  • 阶段二:检查 RDB

    1. 加载 rdb 文件,当进程退出后会加载以前保存的 rdb 文件,rdbLoad()。
  • 阶段三:事件驱动

    1. 创建 event-driven 其实就是解析客户端请,并且将 socket fd 放入到事件驱动当中去,aeCreateFileEvent()。
    2. 执行死循环,接收请求,其实这一步就是我们说 redis 是单线程的原因,aeMain()。
    3. acMain 判断是否停止,如果停止则删除事件,并退出程序,aeDeleteEventLoop()。
int main(int argc, char **argv) {
    //1. 初始化 server 段配置
    initServerConfig();
    //2. 判断是否有指定配置文件,如果有则重新加载配置文件
    if (argc == 2) {
        ResetServerSaveParams();
        loadServerConfig(argv[1]);
    } else if (argc > 2) {
        //如果参数过多则报错,显示正确的配置
        fprintf(stderr, "Usage: ./redis-server [/path/to/redis.conf]\n");
        exit(1);
    }
    //3. 初始化服务
    initServer();
    //4. 判断是否为后台执行
    if (server.daemonize) daemonize();
    redisLog(REDIS_NOTICE, "Server started, Redis version " REDIS_VERSION);
    //5. 加载 rdb 文件,当进程退出后会加载以前保存的 rdb 文件
    if (rdbLoad(server.dbfilename) == REDIS_OK)
        redisLog(REDIS_NOTICE, "DB loaded from disk");
    //6. event-driven 其实就是解析客户端请求
    if (aeCreateFileEvent(server.el, server.fd, AE_READABLE,
                          acceptHandler, NULL, NULL) == AE_ERR)
        oom("creating file event");
    redisLog(REDIS_NOTICE, "The server is now ready to accept connections on port %d", server.port);
    //7. 死循环执行
    aeMain(server.el);
    //8. 当接收到退出信号后,删除事件
    aeDeleteEventLoop(server.el);

    return 0;
}

阶段一:基本初始化

initServerConfig 初始化 Server 配置

initServerConfig 就是对 redis 进行默认值的初始化。

static void initServerConfig() {
    # 初始化 redis 默认 DB 数量,默认是 16 个
    server.dbnum = REDIS_DEFAULT_DBNUM;
        # 初始化 redis 默认端口 3306
    server.port = REDIS_SERVERPORT;
    # 日志级别,默认是 debug
    server.verbosity = REDIS_DEBUG;
    # 默认超时时间
    server.maxidletime = REDIS_MAXIDLETIME;
    # 存储策略
    server.saveparams = NULL;
    server.logfile = NULL; /* NULL = log on standard output */
    server.bindaddr = NULL;
        # 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
    server.glueoutputbuf = 1;
    server.daemonize = 0;
    server.pidfile = "/var/run/redis.pid";
    server.dbfilename = "dump.rdb";
    # 是否要求通行证
    server.requirepass = NULL;
        # 对象共享池
    server.shareobjects = 0;
    # 重置保存策略
    ResetServerSaveParams();
    appendServerSaveParams(60 * 60, 1);  /* save after 1 hour and 1 change */
    appendServerSaveParams(300, 100);  /* save after 5 minutes and 100 changes */
    appendServerSaveParams(60, 10000); /* save after 1 minute and 10000 changes */
    /* 主从复制相关的配置 */
    server.isslave = 0;
    server.masterhost = NULL;
    server.masterport = 6379;
    server.master = NULL;
    server.replstate = REDIS_REPL_NONE;
}

ResetServerSaveParams() 的操作这里很简单,其实就是将原有的内存空间清空。

static void ResetServerSaveParams() {
    zfree(server.saveparams);
    server.saveparams = NULL;
    server.saveparamslen = 0;
}

在这里,在 redis 0.091 版本中,redis 对内存分配使用的还是 zmalloc,这个我们放在后面去讲解,

server 是一个全局静态变量,数据类型是 redisServer。

loadServerConfig 重新加载配置文件

重新加载配置文件就很好理解了。

   if (argc == 2) {
        ResetServerSaveParams();
        loadServerConfig(argv[1]);
    } else if (argc > 2) {
        //如果参数过多则报错,显示正确的配置
        fprintf(stderr, "Usage: ./redis-server [/path/to/redis.conf]\n");
        exit(1);
    }

只是一段非常普通的 if 判断。

如果参数是两个,那么就说明有配置文件,执行加载逻辑

  1. 重新初始化 Server Save 参数
  2. loadServerConfig

loadServerConfig 由于代码太长了,而且大多数都是参数解析,在这里就不全都贴出来了,我们来看一些核心的逻辑。

//跳过了获取文件,判断文件是否有效的逻辑
while (fgets(buf, REDIS_CONFIGLINE_MAX + 1, fp) != NULL) {
        sds *argv;
        int argc, j;

        linenum++;
        line = sdsnew(buf);
        line = sdstrim(line, " \t\r\n");
/* Skip comments and blank lines*/
        if (line[0] == '#' || line[0] == '\0') {
            sdsfree(line);
            continue;
        }

        /* Split into arguments */
        argv = sdssplitlen(line, sdslen(line), " ", 1, &argc);
        sdstolower(argv[0]);

        /* Execute config directives */
        if (!strcmp(argv[0], "timeout") && argc == 2) {
            server.maxidletime = atoi(argv[1]);
            if (server.maxidletime < 1) {
                err = "Invalid timeout value";
                goto loaderr;
            }
                 } else if (xxxx) {
                        .....
                 } else .....
return;
    loaderr:
    fprintf(stderr, "\n*** FATAL CONFIG FILE ERROR ***\n");
    fprintf(stderr, "Reading the configuration file, at line %d\n", linenum);
    fprintf(stderr, ">>> '%s'\n", line);
    fprintf(stderr, "%s\n", err);
    exit(1);
}

在这段,出现了我们 redis 的第一个数据结构——sds,也就是我们在缓存中使用最多的字符串类型。

在 Redis0.091 中,SDS 的实现还是非常简单的,具体的 SDS 操作,我们在后续的部分进行讲解,我们先了解一下这个数据结构。头文件加实现代码一共才 275 行。

2023-03-14T09:02:12.png

Reids 利用 SDS 代替的字符串,这涉及到字符串的二进制安全问题。,C语言中,用“\0”表示字
符串的结束,如果字符串中本身就有“\0”字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。

在 C 语言中,字符串以 \0 结尾,这也就意味着字符串中如果出现了\0就会破坏数据的完整性,字符串也就会被截断。

为了解决这样的问题,redis 将 sds 设计成下面的样子。

typedef char *sds;

struct sdshdr {
    long len;
    long free;
    char buf[];
};
  • len 属性为5,表示这个SDS保存着一个5字节长的字符串,由于长度统计变量的存在,读取字符串不依赖”\0”终止符,保证了二进制安全。
  • free 属性为0,表示这个SDS没有分配任何未使用空间。
  • buf:内容存储在柔性数组 buf中,SDS 对上层暴露的指针不是指向结构体 SDS 的指针,而是直接只想荣幸数组的指针,上层就可以像读取 C 字符串一样读取 SDS 的内容,兼容 C 语言处理字符串的各种函数。

2023-03-14T09:02:32.png

柔性数组只能放在结构体的尾部。柔性数组的地址和接头体是连续的,这样查找内存更快,因为不需要额外通过指针找到字符串为止,可以方便地通过柔性数组的首地址偏移得到结构体首地址,进而方便获取其余变量。

总结一下 SDS 和 Char 的区别

  1. 通过使用SDS字符串(len记录字符串长度),使得获取字符串长度的复杂度从O(N)变为O(1)
  2. 杜绝缓存区溢出,C字符串不记录自身长度,在拼接字符串时可能造成缓存区溢出
  3. 通过未使用空间free,减少修改字符串带来的内存重分配次数

其实这种设计在其他项目中也存在,这是 PHP3 中对 string 类型的视线。

2023-03-14T09:02:47.png

当校验没有问题后,就开始抓好难过解析配置,如果配置正确就继续扫描下一行,如果错误就会利用 goto 跳转到 loaderr。

可能在很多 C 语言的帖子中或者老师的课程中都不允许使用 goto 关键字,但是在实际的项目中这种用法有很多,并不是绝对不可以使用,因为在 C语言中并没有异常机制,像这种利用 goto 特性跳转到输出位置进行输出信息,处理异常会特别方便,代码逻辑也更加的清晰,在处理异常的同事也可以进行回收资源。

老师说的意思其实是不要多层跳转,因为多层跳转会让代码变得混乱。

initServer() 初始化 server

得到配置后,万事俱备只欠东风,我们开始对 server 进行初始化。

static void initServer() {
    int j;

    // 屏蔽信号,避免异常
    signal(SIGHUP, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);

   
    server.clients = listCreate();
    server.slaves = listCreate();
    server.monitors = listCreate();
    server.objfreelist = listCreate();

    // 创建 SharedObjects 这里就是存储 redis 数据的地方;
        //  如果有熟悉Redis的朋友可能会知道 RedisObject,这个就是它的前身。
    createSharedObjects();

        //创建事件驱动
    server.el = aeCreateEventLoop();
    server.db = zmalloc(sizeof(redisDb) * server.dbnum);
    server.sharingpool = dictCreate(&setDictType, NULL);
    server.sharingpoolsize = 1024;

        // 如果分配内存失败也就意味着没有可用空间了,那就 OOM
    if (!server.db || !server.clients || !server.slaves || !server.monitors || !server.el || !server.objfreelist)
        oom("server initialization"); /* Fatal OOM */

    #创建 TCP 连接
    server.fd = anetTcpServer(server.neterr, server.port, server.bindaddr);
    if (server.fd == -1) {
        redisLog(REDIS_WARNING, "Opening TCP port: %s", server.neterr);
        exit(1);
    }

    for (j = 0; j < server.dbnum; j++) {
        server.db[j].dict = dictCreate(&hashDictType, NULL);
        server.db[j].expires = dictCreate(&setDictType, NULL);
        server.db[j].id = j;
    }

    server.cronloops = 0;
    server.bgsaveinprogress = 0;
    server.lastsave = time(NULL);
    server.dirty = 0;
    server.usedmemory = 0;
    server.stat_numcommands = 0;
    server.stat_numconnections = 0;
    server.stat_starttime = time(NULL);
    aeCreateTimeEvent(server.el, 1000, serverCron, NULL, NULL);
}
  • 屏蔽信号
  • 初始化 clients,slaves,monitors,objfreelist 四个链表。

    • clients 客户端
    • slaves 从库
    • monitors 监控器,某个 client 执行 monitors 的时候会变成一个监控器,服务端执行的所有命令都会发送给监控器。
    • objfreelist 空闲对象池
  • 初始化 dict 也就是 shareobjects 存储键值对的地方
  • 创建 tcp 连接
  • 创建数据库,其中会初始化 dict,过期时间 dict
  • 初始化 server 剩余的属性
  • 注册听时期

listCreate() 引出了,我们的第二个数据结构——双向链表。

2023-03-14T09:14:02.png

其代码量加载一起一共是 214 行,核心主要是下面两个结构体,list、ListNode 构成了 redis 中的双向链表。

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned int len;
} list;

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

在链表中保存着 head 和 tail,也就是链表中的头和尾。每个 Node 节点都保存着前一个和后一个的节点指针。

通过单个节点我们不仅能够访问到下一个节点,还能访问到上一个节点。使用双链表而非单链表的目的是想增加结构的灵活性,因为只有经常对数据进行增加或者删除才会用到链表这种数据结构。无论是增加节点抑或删除节点,双链表都要比单链表更加简单高效。但提高灵活性的代价是需要更多的空间,但这个代价小到可以被我们忽略。

daemonize 守护进程

static void daemonize(void) {
    int fd;
    FILE *fp;

    if (fork() != 0) exit(0); /* parent exits */
    setsid(); /* create a new session */

    /* Every output goes to /dev/null. If Redis is daemonized but
     * the 'logfile' is set to 'stdout' in the configuration file
     * it will not log at all. */
    if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        if (fd > STDERR_FILENO) close(fd);
    }
    /* Try to write the pid file */
    fp = fopen(server.pidfile, "w");
    if (fp) {
        fprintf(fp, "%d\n", getpid());
        fclose(fp);
    }

这是一个标准的守护进程代码。

  1. fork 子进程,父进程退出
  2. 设置会话 id
  3. 转移标准输入,标准输出,标准错误
  4. 将 pid 放到文件中

阶段二:rdbLoad 加载 rdb 文件

函数loadDb 的实现完全是根据写入磁盘数据库文件的rdbSave函数来实现的, 后续我们在定时事件中再看rdbSave实现,在此略过分析loadDb的实现。

阶段三:事件驱动

事件驱动框架是 Redis Server 运行的核心,该框架爱一单启动后就会一直执循环执行,每次循环都会处理一批出发的网络读写事件。

其实进入事件框架开始执行并不复杂,main 函数直接调用时间框架爱的主题函数 aeMain 后就进入了时间处理循环了。

//7. 死循环执行
aeMain(server.el);

而他的源代码实现更简单。

void aeMain(aeEventLoop *eventLoop)
{
    eventLoop->stop = 0;
    while (!eventLoop->stop)
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}

其实就是一个死循环,如果当前的事件驱动的 stop 属性等于 1 了就说明服务终止,那么就退出循环,整个业务逻辑就结束了。

//8. 当接收到退出信号后,删除事件
aeDeleteEventLoop(server.el);

在 0.091 这个版本中还没有引入 epoll 的支持,底层使用的还是 select。

2023-03-14T09:11:15.png

所以在这个版本中,如果没有做内核优化,那么 redis 的并发 fd 最多也就是 1024 个,远没有后面那么高的并发量。

到目前为止,我们算是知道了 redis0.091 的入口,接下来,我们会进入到具体的实现细节当中,下一章,我们来看 redis server 是如何处理用户请求的。