当我们对不断加深对某一项技术的了解时,一定会在一个特定的时间对它的实现方式产生兴趣。没错,这就是我现在的状态,所以,多年没有读/写C语言的我,决定要啃一下Redis的源码。
Redis大体上可以分为两部分:服务器和客户端(读者吐槽:你这分的也太大体了吧)。在使用时,我们先启动服务器,然后再启动客户端。由客户端向服务器发送命令,服务器处理后将结果返回给客户端。我们从“头”开始,一起来了解一下Redis服务器在启动的时候都做了哪些事情。
对于C语言来说,main函数是一个程序的的入口,Redis也不例外。Redis的main函数写在server.c文件中。由于redis启动过程相当复杂,需要判断许多条件,例如是否在集群中,或者是否是哨兵模式等等,因此我们只介绍单机redis启动过程中一些比较重要的步骤。
初始化全局服务器状态
如果redis-server命令启动时使用了test参数,那么就会先进行指定的测试。接下来调用了initServerConfig()函数,这个函数初始化了一个类型为redisServer的全局变量server。redisServer这个结构包含了非常多的字段,由于篇幅限制,我们不在这里列出,如果按类别划分的话,可以分为以下类别:
- General
- Modules
- Networking
- RDB / AOF loading information
- Fast pointers to often looked up command
- Fields used only for stats
- Configuration
- AOF / RDB persistence
- Logging
- Replication
- Synchronous replication
- Limits
- Blocked clients
- Sort parameters
- Zip structure config
- time cache
- Pubsub
- Cluster
- Scripting
- Lazy free
- Latency monitor
- Assert & bug reporting
- System hardware info
如果用一句话来概括initServerConfig()函数作用,它就是用来给可以在配置文件(通常命名为redis.conf)中配置的变量初始化一个默认值。比较常用的变量有服务器端口号、日志等级等等。
设置commend table
在initServerConfig()函数中,会调用populateCommandTable()函数来设置服务器的命令表,命令表的结构如下。
1 | struct redisCommand redisCommandTable[] = { |
每一项代表的含义是:
- name:命令的名称
- function:命令对应的函数名。redis-server处理命令时要执行的函数
- arity:命令的参数个数,如果是-N代表大于等于N
- sflags:命令标志,标识命令的类型(read/write/admin…)
- flags:位掩码,由Redis根据sflags计算
- get_keys_proc:可选函数,当下面三个项不能指定哪些参数是key时使用
- first_key_index:第一个是key的参数
- last_key_index:最后一个是key的参数
- key_step:key的“步长”,比如MSET的key_step是2,因为它的参数是key,val,key,val这样的形式
- microseconds:执行命令所需要的微秒数
- calls:该命令被调用总次数
设置好命令表后,redis-server还会对一些常用的命令设置快速查找方式,直接赋予server的成员指针。
1 | server.delCommand = lookupCommandByCString("del"); |
初始化哨兵模式
变量初始化以后,就会将启动命令的路径和参数保存起来,以备下次重启的时候使用。如果启动的服务是哨兵模式,那么就会调用initSentinelConfig()和initSentinel()这两个方法来初始化哨兵模式。对sentinel不了解的同学可以看这里。initSentinelConfig()和initSentinel()都在sentinel.c文件中。initSentinelConfig函数负责初始化sentinel的端口号,以及解除服务器的保护模式。initSentinel函数负责将command table设置为只支持sentinel命令,以及初始化sentinelState数据格式。
修复持久化文件
启动模式如果是redis-check-rdb/aof,那么就会执行redis_check_rdb_main()或redis_check_aof_main()这两个函数来修复持久化文件,不过redis_check_rdb_main函数所做的事情在Redis启动过程中已经做了,所以这里不需要做,直接使这个函数加载错误就可以了。
处理参数
如果是简单的参数例如-v或–version、-h或–help,就会直接调用相应的方法,打印信息。如果是使用其他配置文件,则修改server.exec_argv。对于其他信息,会将他们转换成字符串,然后添加进配置文件,例如“–port 6380”就会被转换成“port 6380\n”加进配置文件。这时,redis就会调用loadServerConfig()函数来加载配置文件,这个过程会覆盖掉前面初始化默认配置文件的变量的值。
initServer()
initServer()函数负责结束server变量初始化工作。首先设置处理信号(SIGHUP和SIGPIPE除外),接着会创建一些双向列表用来跟踪客户端、从节点等。
1 | server.current_client = NULL; |
Shared object
createSharedObjects()函数会创建一些shared对象保存在全局的shared变量中,对于不同的命令,可能会有相同的返回值(比如报错)。这样在返回时就不必每次都去新增对象了,保存到内存中了。这个设计就是以Redis启动时多消耗一些时间为代价,换取运行的更小的延迟。
1 | shared.crlf = createObject(OBJ_STRING,sdsnew("\r\n")); |
Shared integers
除了上述的一些返回值以外,createSharedObjects()函数还会创建一些共享的整数对象。对Redis来说,有许多类型(比如lists或者sets)都需要一些整数(比如数量),这时就可以复用这些已经创建好的整数对象,而不需要重新分配内存并创建。这同样是牺牲了启动时间来换取运行时间。
新增循环事件
initServer()函数调用aeCreateEventLoop()函数(ae.c文件)来增加循环事件,并将结果返回给server的el成员。Redis使用不同的函数来兼容各个平台,在Linux平台使用epoll,在BSD使用kqueue,都不是的话,最终会使用select。Redis轮询新的连接以及I/O事件,有新的事件到来时就会及时作出响应。
分配数据库
Redis初始化需要的数据库,并将结果赋给server的db成员。
1 | server.db = zmalloc(sizeof(redisDb)*server.dbnum); |
监听TCP端口
listenToPort()用来初始化一些文件描述符,从而监听server配置的地址和端口。listenToPort函数会根据参数中的地址判断要监听的是IPv4还是IPv6,对应的调用anetTcpServer()或anetTcp6Server()函数,如果参数中未指明地址,则会强行绑定0.0.0.0
初始化LRU键池
evictionPoolAlloc()(evict.c文件中)用于初始化LRU的键池,Redis的key过期策略是近似LRU算法。
1 | void evictionPoolAlloc(void) { |
Server cron
initServer()函数接下来会为数据库和pub/sub再生成一些列表和字典,重置一些状态,标记系统启动时间。在这之后,Redis会执行aeCreateTimeEvent()(在ae.c文件中)函数,用来新建一个循环执行serverCron()函数的事件。serverCron()默认每100毫秒执行一次。
1 | /* Create the timer callback, this is our way to process many background |
可以看到,代码中创建循环事件时指定每毫秒执行一次serverCron()函数,这是为了使循环马上启动,但是serverCron()函数的返回值又会被作为下次执行的时间间隔。默认为1000/server.hz。server.hz随着客户端数量的增加而增加。
serverCron()函数做了许多定时执行的任务,包括rehash、后台持久化,AOF重新与清理、清理过期key,交换虚拟内存、同步主从节点等等。总之能想到的Redis的定时任务几乎都在serverCron()函数中处理。
打开AOF文件
1 | /* Open the AOF file if needed. */ |
最大内存限制
对于32位系统,最大内存是4GB,如果用户没有明确指出Redis可使用的最大内存,那么这里默认限制为3GB。
1 | /* 32 bit instances are limited to 4GB of address space, so if there is |
Redis Server启动
如果Redis被设置为后台运行,此时Redis会尝试写pid文件,默认路径是/var/run/redis.pid。这时,Redis服务器已经启动,不过还有一些事情要做。
从磁盘加载数据
如果存在AOF文件或者dump文件(都有的话AOF文件的优先级高),loadDataFromDisk()函数负责将数据从磁盘加载到内存。
最后的设置
每次进入循环事件时,要调用beforeSleep()函数,它做了以下这些事情:
- 如果server是cluster中的一个节点,调用clusterBeforeSleep()函数
- 执行一个快速的周期
- 如果有客户端在前一个循环事件被阻塞了,向所有的从节点发送ACK请求
- 取消在同步备份过程中被阻塞的客户端的阻塞状态
- 检查是否有因为阻塞命令而被阻塞的客户端,如果有,解除
- 把AOF缓冲区写到磁盘
- 线程释放GIL
进入主循环事件
程序调用aeMain()函数,进入主循环,这时其他的一些循环事件也会分别被调用
1 | void aeMain(aeEventLoop *eventLoop) { |
到此,Redis服务器已经完全准备好处理各种事件了。后面我们会继续了解Redis命令执行过程究竟做了哪些事情。