2. 初始化服务器默认参数(initServerConfig)
1. Redis服务器初始化流程
redis服务器的初始化流程如下图所示,这里不包含哨兵模式和集群模式的流程,哨兵和集群模式的流程将在后面的博客中介绍。
2. 初始化服务器默认参数(initServerConfig)
第一步是创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值。初始化服务器的状态主要完成的主要工作包括:
(1)设置服务器运行ID(server.runid)为一个根据时间戳生成的一个随机数
(2)设置服务器的serverCron函数的默认运行频率(server.hz)为10次每秒,即100ms调用一次
(3)设置服务器的默认配置文件路径(server.configfile)为空
(4)设置服务器的运行架构,32位或者64位(server.arch_bits),一般为64位
(5)设置服务器的默认端口号(server.port)为6379
(6)设置服务器listen函数的backlog(server.tcp_backlog)为511
(7)设置服务器的默认数据库个数(server.dbnum)为16
(8)设置服务器的日志级别(server.verbosity)为REDIS_NOTICE
(9)设置服务器键的最大空转时长(server.maxidletime)为0
(10)设置服务器TCP的keepalive为0
(11)设置服务器的过期键策略(server.active_expire_enabled)为使能状态
(12)设置客户端最大查询缓冲区大小(server.client_max_querybuf_len)为1GB
(13)设置RDB的保存条件(server.saveparams)为NULL
(14)设置服务器是否使能系统日志(server.syslog_enabled)为0
(15)设置服务器是否作为守护进程运行(server.daemonize)为0
(16)设置AOF持久化策略(server.aof_state)为关闭
(17)设置服务器默认的AOF持久化条件(server.aof_rewrite_perc、server.aof_rewrite_min_size、server.aof_rewrite_base_size)
(18)设置服务器的最大客户端数量(server.maxclients)为1000
(19)设置服务器的最大内存限制(server.maxmemory)为0,即对内存无限制使用
(20)设置服务器的LRU淘汰策略(server.maxmemory_policy)为REDIS_MAXMEMORY_NO_EVICTION
(21)设置服务器的LRU淘汰策略的样本数(server.maxmemory_samples)为5
(22)初始化服务器的LRU时钟(server.lruclock)
(23)设置RDB持久化的保存条件(server.saveparams)
(24)设置和主从复制相关的状态
(25)设置PSYNC命令所使用的backlog
(26)设置客户端输出缓冲区限制(server.client_obuf_limits[])
(27)初始化服务器的命令表字典(server.commands),并且调用函数populateCommandTable把命令表中redisCommandTable的所有命令添加到命令表server.commands字典中,键为命令的名字,值为redisCommand的一个结构体
/*
* Redis 命令
*/
struct redisCommand {
// 命令名字
char *name;
// 实现函数
redisCommandProc *proc;
// 参数个数
int arity;
// 字符串表示的 FLAG
char *sflags; /* Flags as string representation, one char per flag. */
// 实际 FLAG
int flags; /* The actual flags, obtained from the 'sflags' field. */
/* Use a function to determine keys arguments in a command line.
* Used for Redis Cluster redirect. */
// 从命令中判断命令的键参数。在 Redis 集群转向时使用。
redisGetKeysProc *getkeys_proc;
/* What keys should be loaded in background when calling this command? */
// 指定哪些参数是 key
int firstkey; /* The first argument that's a key (0 = no keys) */
int lastkey; /* The last argument that's a key */
int keystep; /* The step between first and last key */
// 统计信息
// microseconds 记录了命令执行耗费的总毫微秒数
// calls 是命令被执行的总次数
long long microseconds, calls;
};
(28)初始化慢查询日志(server.slowlog_log_slower_than、server.slowlog_max_len)
(29)初始化调试选项
3. 加载配置文件(loadServerConfig)
在初始化server变量之后,解析服务器启动时从命令行传入的参数,如果服务器启动时指定了配置文件,则这里就会开始载入用户给定的配置参数和配置文件redis.conf,并根据用户设定的配置,对server变量相关属性值进行更新。
4. 初始化服务器数据结构(initServer)
initServerConfig函数初始化时,程序只创建了命令表一个数据结构,除了这个命令表外,服务器状态还包括其它数据结构需要设置,因此initServer函数的工作如下:
(1)设置信号处理函数:忽略SIGHUP和SIGPIPE两种信号,设置信号SIGTERM的信号处理函数为sigtermHandler,即进程在收到该信号时打印信息,终止服务器
(2)服务器的当前客户端(server.current_client)设置为NULL
(3)服务器的客户端链表(server.clients)初始化,这个链表记录了所有服务器相连的客户端状态结构
(4)服务器的待异步关闭的客户端链表(server.clients_to_close)初始化,这个链表记录了所有待异步关闭的客户端,一般在serverCron函数中关闭
(5)服务器的从服务器链表(server.slaves)初始化,这个链表保存了所有从服务器
(6)服务器的监视器链表(server.monitors)初始化,这个链表保存了所有监视器,即哨兵服务器
(7)服务器的未阻塞的客户端链表(server.unblocked_clients)初始化,这个链表保存了下一个循环前未阻塞的客户端
(8)服务器的等待回复的客户端链表(server.clients_waiting_acks)初始化,这个链表保存了等待回复的客户端列表,即阻塞在“WAIT”命令上的客户端,“WAIT”命令表示主从服务器同步复制,客户端的命令中如果带有“WAIT”,用户指定至少多少个replication成功以及超时时间。
(9)服务器的客户端中是否有等待slave回复的客户端(server.get_ack_from_slaves)初始化为0,如果为1,表示有客户端正在等待从服务器slave的回复。
(10)调用函数createSharedObjects()创建共享对象,通过复用来减少内存碎片:
struct sharedObjectsStruct {
robj *crlf, *ok, *err, *emptybulk, *czero, *cone, *cnegone, *pong, *space,
*colon, *nullbulk, *nullmultibulk, *queued,
*emptymultibulk, *wrongtypeerr, *nokeyerr, *syntaxerr, *sameobjecterr,
*outofrangeerr, *noscripterr, *loadingerr, *slowscripterr, *bgsaveerr,
*masterdownerr, *roslaveerr, *execaborterr, *noautherr, *noreplicaserr,
*busykeyerr, *oomerr, *plus, *messagebulk, *pmessagebulk, *subscribebulk,
*unsubscribebulk, *psubscribebulk, *punsubscribebulk, *del, *rpop, *lpop,
*lpush, *emptyscan, *minstring, *maxstring,
*select[REDIS_SHARED_SELECT_CMDS],
*integers[REDIS_SHARED_INTEGERS],
*mbulkhdr[REDIS_SHARED_BULKHDR_LEN], /* "*<value>\r\n" */
*bulkhdr[REDIS_SHARED_BULKHDR_LEN]; /* "$<value>\r\n" */
};
(11)调用函数adjustOpenFilesLimit()根据服务器最大客户端数量(server.maxclients)调整一个进程最大可以打开的文件个数
(12)调用函数aeCreateEventLoop()初始化服务器的事件循环结构体(server.el):
/* File event structure
*
* 文件事件结构
*/
typedef struct aeFileEvent {
/* 监听事件类型掩码,值可以是 AE_READABLE 或 AE_WRITABLE ,或者 AE_READABLE | AE_WRITABLE */
int mask;
aeFileProc *rfileProc; // 读事件处理器
aeFileProc *wfileProc; // 写事件处理器
void *clientData; // 多路复用库的私有数据
} aeFileEvent;
/* Time event structure
*
* 时间事件结构
*/
typedef struct aeTimeEvent {
long long id; // 时间事件的唯一标识符
// 事件的到达时间
long when_sec; /* seconds */
long when_ms; /* milliseconds */
// 事件处理函数
aeTimeProc *timeProc;
aeEventFinalizerProc *finalizerProc; // 事件释放函数
void *clientData; // 多路复用库的私有数据
struct aeTimeEvent *next; // 指向下个时间事件结构,形成链表
} aeTimeEvent;
/* A fired event
*
* 已就绪事件
*/
typedef struct aeFiredEvent {
int fd; // 已就绪文件描述符
/* 事件类型掩码,值可以是 AE_READABLE 或 AE_WRITABLE,或者是两者的或 */
int mask;
} aeFiredEvent;
/* State of an event based program
*
* 事件处理器的状态
*/
typedef struct aeEventLoop {
int maxfd; // 目前已注册的最大描述符
int setsize; // 目前已追踪的最大描述符
long long timeEventNextId; // 用于生成时间事件 id
time_t lastTime; // 最后一次执行时间事件的时间
aeFileEvent *events; // 已注册的文件事件
aeFiredEvent *fired; // 已就绪的文件事件
aeTimeEvent *timeEventHead; // 时间事件
int stop; // 事件处理器的开关
void *apidata; // 多路复用库的私有数据
aeBeforeSleepProc *beforesleep; // 在处理事件前要执行的函数
} aeEventLoop;
函数中需要对结构体进行初始化,包括:
- 初始化文件事件结构体数组:eventLoop->events
- 初始化已就绪文件事件结构体数组:eventLoop->fired
- 设置已追踪的最大描述符大小为server.maxclients + REDIS_EVENTLOOP_FDSET_INCR
- 初始化时间时间结构eventLoop->timeEventHead、eventLoop->timeEventNextId等
- 调用函数aeApiCreate()创建epoll句柄,并初始化eventLoop->apidata
- 调用函数 listenToPort()创建套接字,并且打开监听端口
(13)根据数据库的数量(server.dbnum)给服务器的数据库数组(server.db)分配内存,这个数组包含了服务器的所有数据库,并且初始化数组中每个元素的所有字典项:
typedef struct redisDb {
dict *dict; // 数据库键空间,保存着数据库中的所有键值对
dict *expires; // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
dict *blocking_keys; // 正处于阻塞状态的键
dict *ready_keys; // 可以解除阻塞的键
dict *watched_keys; // 正在被 WATCH 命令监视的键
struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */
int id; // 数据库号码
long long avg_ttl; // 数据库的键的平均 TTL ,统计信息
} redisDb;
(14)初始化server.pubsub_channels和server.pubsub_patterns:用于保存频道订阅信息和模式订阅信息
(15)初始化server.lua:保存用于执行Lua脚本的Lua环境
(16)初始化server.slowlog:用于保存慢查询日志
(17)调用函数aeCreateTimeEvent()注册时间事件,将函数serverCron()注册给时间事件处理器,并且设定1ms后执行这个函数,并且将注册的时间时间放在事件处理器的时间事件链表的表头节点,这个函数后面的博客会介绍
(18)调用函数aeCreateFileEvent()为监听套接字关联连接应答处理器,这个函数做的事儿为:
- 将监听的套接字加入到epoll的句柄中,并且将事件类型mask设置为AE_READABLE;
- 取出套接字在事件处理器中对应的文件事件aeFileEvent,并且初始化文件事件的事件类型mask设置为AE_READABLE,文件事件的读事件处理器和写事件处理器设置为acceptTcpHandler(),这个函数后面的博客中会介绍;
(19)如果AOF持久化功能打开,那么打开现有的AOF文件,如果AOF文件不存在,那么创建并打开一个新的AOF文件,为AOF写入做好准备
(20)如果是32位的架构,则设置服务器的最大内存(server.maxmemory)为3GB,内存淘汰策略为REDIS_MAXMEMORY_NO_EVICTION,如果是64位架构,则没这个限制
(21)初始化Lua脚本系统:scriptingInit()
(22)初始化慢查询功能:slowlogInit()
(23)初始化服务器的后台异步I/O模块,为将来的I/O操作做好准备:bioInit(),这里的BIO模块其实是创建一个线程池,线程数量大小为2(REDIS_BIO_NUM_OPS),每个线程会处理一种类型的后台处理任务,分别是关闭文件任务和调用fdatasync,将AOF文件缓冲区的内容写入到磁盘 。线程池中需要使用到两个互斥量,分别用于对两个任务队列进行加锁,需要使用到两个条件变量。
5. 还原数据库状态(loadDataFromDisk)
完成对server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。
(1)如果开启了AOF持久化功能,那么会优先使用AOF文件来恢复数据库,调用函数为:loadAppendOnlyFile(),这个函数将在后面介绍。
(2)如果没有开启AOF持久化功能,就会使用RDB文件来恢复数据库,调用函数为:rdbLoad(),这个函数将在后面介绍。
6. 执行事件主循环(aeMain)
/*
* 事件处理器的主循环
*/
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 如果有需要在事件处理前执行的函数,那么运行它
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 开始处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
在一个循环中对事件进行处理,下一节中介绍。