Redis服务器负责接收处理用户请求,为用户提供服务。

Redis服务器的启动命令格式如下:

redis-server [ configfile ] [ options ]

configfile参数指定配置文件。options参数指定启动配置项,它可以覆盖配置文件中的配置项,如

redis-server /path/to/redis.conf --port 7777 --protected-mode no

该命令启动Redis服务,并指定了配置文件/path/to/redis.conf,给出了两个启动配置项:port、protected-mode。

本文通过阅读Redis源码,分析Redis启动过程,内容摘自新书《Redis核心原理与实践》。

本文涉及Redis的很多概念,如事件循环器、ACL、Module、LUA、慢日志等,这些功能在作者新书《Redis核心原理与实践》做了详尽分析,感兴趣的读者可以参考本书。

服务器定义

提示:本章代码如无特殊说明,均在server.h、server.c中。

Redis中定义了server.h/redisServer结构体,存储Redis服务器信息,包括服务器配置项和运行时数据(如网络连接信息、数据库redisDb、命令表、客户端信息、从服务器信息、统计信息等数据)。

struct redisServer {
pid_t pid;
pthread_t main_thread_id;
char *configfile;
char *executable;
char **exec_argv;
...
}

redisServer中的属性很多,这里不一一列举,等到分析具体功能时再说明相关的server属性。

server.h中定义了一个redisServer全局变量:

extern struct redisServer server;

本书说到的server变量,如无特殊说明,都是指该redisServer全局变量。例如,第1部分说过server.list_max_ziplist_size等属性,正是指该变量的属性。

可以使用INFO命令获取服务器的信息,该命令主要返回以下信息:

  • server:有关Redis服务器的常规信息。
  • clients:客户端连接信息。
  • memory:内存消耗相关信息。
  • persistence:RDB和AOF持久化信息。
  • stats:常规统计信息。
  • replication:主/副本复制信息。
  • cpu:CPU消耗信息。
  • commandstats:Redis 命令统计信息。
  • cluster:Redis Cluster集群信息。
  • modules:Modules模块信息。
  • keyspace:数据库相关的统计信息。
  • errorstats:Redis错误统计信息。

INFO命令响应内容中除了memory和cpu等统计数据,其他数据大部分都保存在redisServer中。

main函数

server.c/main函数负责启动Redis服务:

int main(int argc, char **argv) {
...
// [1]
server.sentinel_mode = checkForSentinelMode(argc,argv);
// [2]
initServerConfig();
ACLInit(); moduleInitModulesSystem();
tlsInit(); // [3]
server.executable = getAbsolutePath(argv[0]);
server.exec_argv = zmalloc(sizeof(char*)*(argc+1));
server.exec_argv[argc] = NULL;
for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]); // [4]
if (server.sentinel_mode) {
initSentinelConfig();
initSentinel();
} // [5]
if (strstr(argv[0],"redis-check-rdb") != NULL)
redis_check_rdb_main(argc,argv,NULL);
else if (strstr(argv[0],"redis-check-aof") != NULL)
redis_check_aof_main(argc,argv); // more
}

【1】检查该Redis服务器是否以sentinel模式启动。

【2】initServerConfig函数将redisServer中记录配置项的属性初始化为默认值。ACLInit函数初始化ACL机制,moduleInitModulesSystem函数初始化Module机制。

【3】记录Redis程序可执行路径及启动参数,以便后续重启服务器。

【4】如果以Sentinel模式启动,则初始化Sentinel机制。

【5】如果启动程序是redis-check-rdb或redis-check-aof,则执行redis_check_rdb_main或redis_check_aof_main函数,它们尝试检验并修复RDB、AOF文件后便退出程序。

Redis编译完成后,会生成5个可执行程序:

  • redis-server:Redis执行程序。
  • redis-sentinel:Redis Sentinel执行程序。
  • redis-cli:Redis客户端程序。
  • redis-benchmark:Redis性能压测工具。

    redis-check-aof、redis-check-rdb:用于检验和修复RDB、AOF持久化文件的工具。

    继续分析main函数:
int main(int argc, char **argv) {
...
if (argc >= 2) {
j = 1;
sds options = sdsempty();
char *configfile = NULL; // [6]
if (strcmp(argv[1], "-v") == 0 ||
strcmp(argv[1], "--version") == 0) version();
... // [7]
if (argv[j][0] != '-' || argv[j][1] != '-') {
configfile = argv[j];
server.configfile = getAbsolutePath(configfile);
zfree(server.exec_argv[j]);
server.exec_argv[j] = zstrdup(server.configfile);
j++;
} // [8]
while(j != argc) {
...
}
// [9]
if (server.sentinel_mode && configfile && *configfile == '-') {
...
exit(1);
}
// [10]
resetServerSaveParams();
loadServerConfig(configfile,options);
sdsfree(options);
}
...
}

【6】对-v、--version、--help、-h、--test-memory等命令进行优先处理。

strcmp函数比较两个字符串str1、str2,若str1=str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数。

【7】如果启动命令的第二个参数不是以“--”开始的,则是配置文件参数,将配置文件路径转化为绝对路径,存入server.configfile中。

【8】读取启动命令中的启动配置项,并将它们拼接到一个字符串中。

【9】以Sentinel模式启动,必须指定配置文件,否则直接报错退出。

【10】config.c/resetServerSaveParams函数重置server.saveparams属性(该属性存放RDB SAVE配置)。config.c/loadServerConfig函数从配置文件中加载所有配置项,并使用启动命令配置项覆盖配置文件中的配置项。

提示:config.c中的configs数组定义了大多数配置选项与server属性的对应关系:

standardConfig configs[] = {
createBoolConfig("rdbchecksum", NULL, IMMUTABLE_CONFIG, server.rdb_checksum, 1, NULL, NULL),
createBoolConfig("daemonize", NULL, IMMUTABLE_CONFIG, server.daemonize, 0, NULL, NULL),
...
}

配置项rdbchecksum对应server.rdb_checksum属性,默认值为1(即bool值yes),其他配置项以此类推。如果读者需要查找配置项对应的server属性和默认值,则可以从中查找。

下面继续分析main函数:

int main(int argc, char **argv) {
...
// [11]
server.supervised = redisIsSupervised(server.supervised_mode);
int background = server.daemonize && !server.supervised;
if (background) daemonize();
// [12]
serverLog(LL_WARNING, "oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo");
... // [13]
initServer();
if (background || server.pidfile) createPidFile();
... if (!server.sentinel_mode) {
...
// [14]
moduleLoadFromQueue();
ACLLoadUsersAtStartup();
InitServerLast();
loadDataFromDisk();
if (server.cluster_enabled) {
if (verifyClusterConfigWithData() == C_ERR) {
...
exit(1);
}
}
...
} else {
// [15]
InitServerLast();
sentinelIsRunning();
...
} ...
// [16]
redisSetCpuAffinity(server.server_cpulist);
setOOMScoreAdj(-1);
// [17]
aeMain(server.el);
// [18]
aeDeleteEventLoop(server.el);
return 0;
}

【11】server.supervised属性指定是否以upstart服务或systemd服务启动Redis。如果配置了server.daemonize且没有配置server.supervised,则以守护进程的方式启动Redis。

【12】打印启动日志。

【13】initServer函数初始化Redis运行时数据,createPidFile函数创建pid文件。

【14】如果非Sentinel模式启动,则完成以下操作:

(1)moduleLoadFromQueue函数加载配置文件指定的Module模块;

(2)ACLLoadUsersAtStartup函数加载ACL用户控制列表;

(3)InitServerLast函数负责创建后台线程、I/O线程,该步骤需在Module模块加载后再执行;

(4)loadDataFromDisk函数从磁盘中加载AOF或RDB文件。

(5)如果以Cluster模式启动,那么还需要验证加载的数据是否正确。

【15】如果以Sentinel模式启动,则调用sentinelIsRunning函数启动Sentinel机制。

【16】尽可能将Redis主线程绑定到server.server_cpulist配置的CPU列表上,Redis 4开始使用多线程,该操作可以减少不必要的线程切换,提高性能。

【17】启动事件循环器。事件循环器是Redis中的重要组件。在Redis运行期间,由事件循环器提供服务。

【18】执行到这里,说明Redis服务已停止,aeDeleteEventLoop函数清除事件循环器中的事件,最后退出程序。

Redis初始化过程

下面看一下initServer函数,它负责初始化Redis运行时数据:

void initServer(void) {
int j;
// [1]
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
setupSignalHandlers();
// [2]
makeThreadKillable();
// [3]
if (server.syslog_enabled) {
openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT,
server.syslog_facility);
} // [4]
server.aof_state = server.aof_enabled ? AOF_ON : AOF_OFF;
server.hz = server.config_hz;
server.pid = getpid();
... // [5]
createSharedObjects();
adjustOpenFilesLimit();
// [6]
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
if (server.el == NULL) {
...
exit(1);
} // more
}

【1】设置UNIX信号处理函数,使Redis服务器收到SIGINT信号后退出程序。

【2】设置线程随时响应CANCEL信号,终止线程,以便停止程序。

【3】如果开启了Unix系统日志,则调用openlog函数与Unix系统日志建立输出连接,以便输出系统日志。

【4】初始化server中负责存储运行时数据的相关属性。

【5】createSharedObjects函数创建共享数据集,这些数据可在各场景中共享使用,如小数字0~9999、常用字符串+OK\r\n(命令处理成功响应字符串)、+PONG\r\n(ping命令响应字符串)。adjustOpenFilesLimit函数尝试修改环境变量,提高系统允许打开的文件描述符上限,避免由于大量客户端连接(Socket文件描述符)导致错误。

【6】创建事件循环器。

UNIX编程:信号也称为软中断,信号是UNIX提供的一种处理异步事件的方法,程序通过设置回调函数告诉系统内核,在信号产生后要做什么操作。系统中很多场景会产生信号,例如:

  • 用户按下某些终端键,使终端产生信号。例如,用户在终端按下了中断键(一般为Ctrl+C组合键),会发送SIGINT信号通知程序停止运行。
  • 系统中发生了某些特定事件,例如,当alarm函数设置的定时器超时,内核发送SIGALRM信号,或者一个进程终止时,内核发送SIGCLD信号给其父进程。
  • 某些硬件异常,例如,除数为0、无效的内存引用。
  • 程序中使用函数发送信号,例如,调用kill函数将任意信号发送给另一个进程。

    感兴趣的读者可以自行深入了解UNIX编程相关内容。

接着分析initServer函数:

void initServer(void) {
server.db = zmalloc(sizeof(redisDb)*server.dbnum); // [7]
if (server.port != 0 &&
listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
exit(1);
... // [8]
for (j = 0; j < server.dbnum; j++) {
server.db[j].dict = dictCreate(&dbDictType,NULL);
server.db[j].expires = dictCreate(&keyptrDictType,NULL);
...
} // [9]
evictionPoolAlloc();
server.pubsub_channels = dictCreate(&keylistDictType,NULL);
server.pubsub_patterns = listCreate();
...
}

【7】如果配置了server.port,则开启TCP Socket服务,接收用户请求。如果配置了server.tls_ port,则开启TLS Socket服务,Redis 6.0开始支持TLS连接。如果配置了server.unixsocket,则开启UNIX Socket服务。如果上面3个选项都没有配置,则报错退出。

【8】初始化数据库server.db,用于存储数据。

【9】evictionPoolAlloc函数初始化LRU/LFU样本池,用于实现LRU/LFU近似算法。

继续初始化server中存储运行时数据的相关属性:

void initServer(void) {
...
// [10]
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
} // [11]
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.");
}
}
... // [12]
aeSetBeforeSleepProc(server.el,beforeSleep);
aeSetAfterSleepProc(server.el,afterSleep); // [13]
if (server.aof_state == AOF_ON) {
server.aof_fd = open(server.aof_filename,
O_WRONLY|O_APPEND|O_CREAT,0644);
...
} // [14]
if (server.arch_bits == 32 && server.maxmemory == 0) {
...
server.maxmemory = 3072LL*(1024*1024); /* 3 GB */
server.maxmemory_policy = MAXMEMORY_NO_EVICTION;
}
// [15]
if (server.cluster_enabled) clusterInit();
replicationScriptCacheInit();
scriptingInit(1);
slowlogInit();
latencyMonitorInit();
}

【10】创建一个时间事件,执行函数为serverCron,负责处理Redis中的定时任务,如清理过期数据、生成RDB文件等。

【11】分别为TCP Socket、TSL Socks、UNIX Socket注册监听AE_READABLE类型的文件事件,事件处理函数分别为acceptTcpHandler、acceptTLSHandler、acceptUnixHandler,这些函数负责接收Socket中的新连接,本书后续会详细分析acceptTcpHandler函数。

【12】注册事件循环器的钩子函数,事件循环器在每次阻塞前后都会调用钩子函数。

【13】如果开启了AOF,则预先打开AOF文件。

【14】如果Redis运行在32位操作系统上,由于32位操作系统内存空间限制为4GB,所以将Redis使用内存限制为3GB,避免Redis服务器因内存不足而崩溃。

【15】如果以Cluster模式启动,则调用clusterInit函数初始化Cluster机制。

  • replicationScriptCacheInit函数初始化server.repl_scriptcache_dict属性。
  • scriptingInit函数初始化LUA机制。
  • slowlogInit函数初始化慢日志机制。
  • latencyMonitorInit函数初始化延迟监控机制。

总结:

  • redisServer结构体存储服务端配置项、运行时数据。
  • server.c/main是Redis启动方法,负责加载配置,初始化数据库,启动网络服务,创建并启动事件循环器。

文章最后,介绍一下新书《Redis核心原理与实践》,本书通过深入分析Redis 6.0源码,总结了Redis核心功能的设计与实现。通过阅读本书,读者可以深入理解Redis内部机制及最新特性,并学习到Redis相关的数据结构与算法、Unix编程、存储系统设计,分布式系统架构等一系列知识。

经过该书编辑同意,我会继续在个人技术公众号(binecy)发布书中部分章节内容,作为书的预览内容,欢迎大家查阅,谢谢。

语雀平台预览:《Redis核心原理与实践》

京东链接

Redis核心原理与实践--Redis启动过程源码分析的更多相关文章

  1. 新书介绍 -- 《Redis核心原理与实践》

    大家好,今天给大家介绍一下我的新书 -- <Redis核心原理与实践>. 后端开发的同学应该对Redis都不陌生,Redis由于性能极高.功能强大,已成为业界非常流行的内存数据库. < ...

  2. Redis核心原理与实践--字符串实现原理

    Redis是一个键值对数据库(key-value DB),下面是一个简单的Redis的命令: > SET msg "hello wolrd" 该命令将键"msg&q ...

  3. Redis核心原理与实践--列表实现原理之ziplist

    列表类型可以存储一组按插入顺序排序的字符串,它非常灵活,支持在两端插入.弹出数据,可以充当栈和队列的角色. > LPUSH fruit apple (integer) 1 > RPUSH ...

  4. Redis核心原理与实践--列表实现原理之quicklist结构

    在上一篇文章<Redis列表实现原理之ziplist结构>,我们分析了ziplist结构如何使用一块完整的内存存储列表数据. 同时也提出了一个问题:如果链表很长,ziplist中每次插入或 ...

  5. Redis核心原理与实践--散列类型与字典结构实现原理

    Redis散列类型可以存储一组无序的键值对,它特别适用于存储一个对象数据. > HSET fruit name apple price 7.6 origin china 3 > HGET ...

  6. Redis核心原理与实践--事务实践与源码分析

    Redis支持事务机制,但Redis的事务机制与传统关系型数据库的事务机制并不相同. Redis事务的本质是一组命令的集合(命令队列).事务可以一次执行多个命令,并提供以下保证: (1)事务中的所有命 ...

  7. Spark(四十九):Spark On YARN启动流程源码分析(一)

    引导: 该篇章主要讲解执行spark-submit.sh提交到将任务提交给Yarn阶段代码分析. spark-submit的入口函数 一般提交一个spark作业的方式采用spark-submit来提交 ...

  8. Activity启动过程源码分析(Android 8.0)

    Activity启动过程源码分析 本文来Activity的启动流程,一般我们都是通过startActivity或startActivityForResult来启动目标activity,那么我们就由此出 ...

  9. Netty入门一:服务端应用搭建 & 启动过程源码分析

    最近周末也没啥事就学学Netty,同时打算写一些博客记录一下(写的过程理解更加深刻了) 本文主要从三个方法来呈现:Netty核心组件简介.Netty服务端创建.Netty启动过程源码分析 如果你对Ne ...

随机推荐

  1. MySQL实战45讲(16--20)-笔记

    目录 16 | "order by"是怎么工作的? 全字段排序 rowid 排序 17 | 如何正确地显示随机消息? 内存临时表 磁盘临时表 随机排序方法 18 | 为什么这些SQ ...

  2. excel中if函数的用法

    IF函数有三个参数,语法如下: =IF(条件判断, 结果为真返回值, 结果为假返回值) 第一参数是条件判断,比如说"A1="百度""或"21>3 ...

  3. vue-cli3 创建多页面应用项目

    1.创建vue项目 cmd命令执行  vue create ruc-continuing  创建vue项目,项目名称:ruc-continuing 选择一个 preset(预置项),或自定义: 选择自 ...

  4. 308 day06_线程、同步

      day06 [线程.同步] 主要内容 线程 同步 线程状态 教学目标 能够描述Java中多线程运行原理 能够使用继承类的方式创建多线程 能够使用实现接口的方式创建多线程 能够说出实现接口方式的好处 ...

  5. 解决idea debugger Frames are not available

    现象:idea2017.3.7 sofaboot项目debugger报错 Frames are not available. 之前好用,不知道为啥突然不能debugger,run能正常运行代码.如下图 ...

  6. Java on Visual Studio Code的更新 – 2021年8月

    Nick Senior Program Manager, Developer Division at Microsoft 大家好,欢迎来到 8 月版的 Visual Studio Code Java ...

  7. 一起学习PHP中GD库的使用(一)

    又到了一个大家非常熟悉的库了,对于图像图形的处理来说,GD 库是 PHPer 们绕不过去的一道坎.从很早很早的 CMS 或者 Discuz 时代,各类开源软件在安装的时候就会明确地指出 GD 库是它们 ...

  8. PHP的Mcrypt加密扩展知识了解

    今天我们来学习的是 PHP 中的一个过时的扩展 Mcrypt .在 PHP7 之前,这个扩展是随 PHP 安装包一起内置发布的,但是现在新版本的 PHP 中已经没有了,需要使用这个扩展的话我们需要单独 ...

  9. 优雅地创建未定义类PHP对象

    在PHP中,如果没有事先准备好类,需要创建一个未定义类的对象,我们可以采用下面三种方式: new stdClass() new class{} (object)[] 首先是stdClass,这个类是一 ...

  10. linux7可以通过远程和localhost访问mysql,但是127.0.0.1不能访问

    网上搜索的其他方法都试过,不行 比如设置权限,开放端口,配置数据库... 最好偶然一个搜索查看可能原因是防火墙端口问题: vim /etc/sysconfig/iptables 在文件中添加下面语句 ...