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. 八、Abp vNext 基础篇丨标签聚合功能

    介绍 本章节先来把上一章漏掉的上传文件处理下,然后实现Tag功能. 上传文件 上传文件其实不含在任何一个聚合中,它属于一个独立的辅助性功能,先把抽象接口定义一下,在Bcvp.Blog.Core.App ...

  2. JS边角料: NodeJS+AutoJS+WebSocket+TamperMonkey实现局域网多端文字互传

    ---阅读时间约 7 分钟,复现时间约 15 分钟--- 由于之前一直在用的扩展 QPush 停止服务了,苦于一人凑齐了 Window, Android, Mac, ios 四种系统的设备,Apple ...

  3. 通过url把第一个页面的数据传到第二页面

    第一个页面: function GetQueryString(name) { var reg = new RegExp("(^|&)"+ name +"=([^& ...

  4. 【第十九篇】- Maven NetBeans之Spring Cloud直播商城 b2b2c电子商务技术总结

    Maven NetBeans NetBeans 6.7 及更新的版本已经内置了 Maven.对于以前的版本,可在插件管理中心获取 Maven 插件.此例中我们使用的是 NetBeans 6.9. 关于 ...

  5. 通过mstsc复制粘贴失败需要重新启动RDP剪切板监视程序rdpclip.exe

    先结束程序 再重新启动程序

  6. 第十一章 Net 5.0 快速开发框架 YC.Boilerplate --图数据库模块Neo4j

    在线文档:http://doc.yc-l.com/#/README 在线演示地址:http://yc.yc-l.com/#/login 源码github:https://github.com/linb ...

  7. pip安装更换国内源

    镜像地址:阿里云 https://mirrors.aliyun.com/pypi/simple/豆瓣http://pypi.douban.com/simple/清华大学 https://pypi.tu ...

  8. c++ undefined reference

    记录一次c++编程时发现的问题 报错 undefined reference undefined reference to `Student::~Student()' 下面还有类似的好几行,翻译过来就 ...

  9. 使用manacher算法解决最长回文子串问题

    要解决的问题 求一个字符串最长回文子串是什么.且时间复杂度 O(N) 具体描述可参考: LeetCode_5_最长回文子串 LintCode_200_最长回文子串 暴力解法 以每个字符为中心向左右两边 ...

  10. js 签字插件

    1.jq-signature  http://bencentra.github.io/jq-signature/    支持的jquery版本低 2.HTML5 canvas   http://www ...