前面的章节介绍了所有 Redis 的重要功能组件:数据结构、数据类型、事务、Lua 环境、事件处理、数据库、持久化,等等,但是我们还没有对 Redis 服务器本身做任何介绍。
不过,服务器本身并没有多少需要介绍的新东西,因为服务器除了维持服务器状态之外,最重要的就是将前面介绍过的各个功能模块组合起来,而这些功能模块在前面的章节里已经介绍过了,所以本章将焦点放在服务器的初始化过程,以及服务器对命令的处理过程上。
本章首先介绍服务器的初始化操作,观察一个 Redis 服务器从启动到可以接受客户端连接,需要经过什么步骤。
然后介绍客户端是如何连接到服务器的,而服务器又是如何维持多个客户端的不同状态的。
文章最后将介绍命令从发送到处理的整个过程,并列举了一个 SET
命令的执行过程作为例子。
从启动 Redis 服务器,到服务器可以接受外来客户端的网络连接这段时间,Redis 需要执行一系列初始化操作。
整个初始化过程可以分为以下六个步骤:
以下各个小节将介绍 Redis 服务器初始化的各个步骤。
redis.h/redisServer
结构记录了和服务器相关的所有数据,这个结构主要包含以下信息:
Note
为了简洁起见,上面只列出了单机情况下的 Redis 服务器信息,不包含 SENTINEL 、 MONITOR 、 CLUSTER 等功能的信息。
在这一步,程序创建一个 redisServer
结构的实例变量 server
用作服务器的全局状态,并将 server
的各个属性初始化为默认值。
当 server
变量的初始化完成之后,程序进入服务器初始化的下一步:读入配置文件。
在初始化服务器的上一步中,程序为 server
变量(也即是服务器状态)的各个属性设置了默认值,但这些默认值有时候并不是最合适的:
等等。
为了让使用者按自己的要求配置服务器,Redis 允许用户在运行服务器时,提供相应的配置文件(config file)或者显式的选项(option),Redis 在初始化完 server
变量之后,会读入配置文件和选项,然后根据这些配置来对 server
变量的属性值做相应的修改:
如果单纯执行 redis-server
命令,那么服务器以默认的配置来运行 Redis 。
另一方面, 如果给 Redis 服务器送入一个配置文件, 那么 Redis 将按配置文件的设置来更新服务器的状态。
比如说, 通过命令 redis-server /etc/my-redis.conf
, Redis 会根据 my-redis.conf
文件的内容来对服务器状态做相应的修改。
除此之外, 还可以显式地给服务器传入选项, 直接修改服务器配置。
举个例子, 通过命令 redis-server --port 10086
, 可以让 Redis 服务器端口变更为 10086
。
当然, 同时使用配置文件和显式选项也是可以的, 如果文件和选项有冲突的地方, 那么优先使用选项所指定的配置值。
举个例子, 如果运行命令 redis-server /etc/my-redis.conf --port 10086
, 并且 my-redis.conf
也指定了 port
选项, 那么服务器将优先使用 --port 10086
(实际上是选项指定的值覆盖了配置文件中的值)。
Redis 默认以 daemon 进程的方式运行。
当服务器初始化进行到这一步时,程序将创建 daemon 进程来运行 Redis ,并创建相应的 pid 文件。
在这一步,初始化程序完成两件事:
server
变量的数据结构子属性分配内存。为数据结构分配内存,并初始化这些数据结构,等同于对相应的功能进行初始化。
比如说,当为订阅与发布所需的链表分配内存之后,订阅与发布功能就处于就绪状态,随时可以为 Redis 所用了。
在这一步,程序完成的主要动作如下:
完成这一步之后,服务器打印出 Redis 的 ASCII LOGO 、服务器版本等信息,表示所有功能模块已经就绪,可以等待被使用了:
_._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 2.9.7 (7a47887b/1) 32 bit .-`` .-```. ```\/ _.,_ ''-._ ( ' , .-` | `, ) Running in stand alone mode |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 | `-._ `._ / _.-' | PID: 6717 `-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | http://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-'
虽然所有功能已经就绪,但这时服务器的数据库还是一片空白,程序还需要将服务器上一次执行时记录的数据载入到当前服务器中,服务器的初始化才算真正完成。
在这一步,程序需要将持久化在 RDB 或者 AOF 文件里的数据,载入到服务器进程里面。
如果服务器有启用 AOF 功能的话,那么使用 AOF 文件来还原数据;否则,程序使用 RDB 文件来还原数据。
当执行完这一步时,服务器打印出一段载入完成信息:
[6717] 22 Feb 11:59:14.830 * DB loaded from disk: 0.068 seconds
到了这一步,服务器的初始化已经完成,程序打开事件循环,开始接受客户端连接。
以下是服务器在这一步打印的信息:
[6717] 22 Feb 11:59:14.830 * The server is now ready to accept connections on port 6379
以下是初始化完成之后,服务器状态和各个模块之间的关系图:
当 Redis 服务器完成初始化之后,它就准备好可以接受外来客户端的连接了。
当一个客户端通过套接字函数 connect
到服务器时,服务器执行以下步骤:
accept
客户端连接,并返回一个套接字描述符 fd
。fd
创建一个对应的 redis.h/redisClient
结构实例,并将该实例加入到服务器的已连接客户端的链表中。fd
关联读文件事件。完成这三步之后,服务器就可以等待客户端发来命令请求了。
Redis 以多路复用的方式来处理多个客户端,为了让多个客户端之间独立分开、不互相干扰,服务器为每个已连接客户端维持一个 redisClient
结构,从而单独保存该客户端的状态信息。
redisClient
结构主要包含以下信息:
Note
为了简洁起见,上面列出的客户端结构信息不包含复制(replication)的相关属性。
当客户端连上服务器之后,客户端就可以向服务器发送命令请求了。
从客户端发送命令请求,到命令被服务器处理、并将结果返回客户端,整个过程有以下步骤:
redisClient
结构的查询缓存中。server
变量,并将命令的执行结果保存到客户端 redisClient
结构的回复缓存中,然后为该客户端的 fd
关联写事件。fd
的写事件就绪时,将回复缓存中的命令结果传回给客户端。至此,命令执行完毕。为了更直观地理解命令执行的整个过程,我们用一个实际执行 SET 命令的例子来讲解命令执行的过程。
假设现在客户端 C1 是连接到服务器 S 的一个客户端,当用户执行命令 SET YEAR 2013
时,客户端调用写入函数,将协议内容 *3\r\n$3\r\nSET\r\n$4\r\nYEAR\r\n$4\r\n2013\r\n"
写入连接到服务器的套接字中。
当 S 的文件事件处理器执行时,它会察觉到 C1 所对应的读事件已经就绪,于是它将协议文本读入,并保存在查询缓存。
通过对查询缓存进行分析(parse),服务器在命令表中查找 SET
字符串所对应的命令实现函数,最终定位到 t_string.c/setCommand
函数,另外,两个命令参数 YEAR
和 2013
也会以字符串的形式保存在客户端结构中。
接着,程序将客户端、要执行的命令、命令参数等送入命令执行器:执行器调用 setCommand
函数,将数据库中 YEAR
键的值修改为 2013
,然后将命令的执行结果保存在客户端的回复缓存中,并为客户端 fd
关联写事件,用于将结果回写给客户端。
因为 YEAR
键的修改,其他和数据库命名空间相关程序,比如 AOF 、REPLICATION 还有事务安全性检查(是否修改了被 WATCH
监视的键?)也会被触发,当这些后续程序也执行完毕之后,命令执行器退出,服务器其他程序(比如时间事件处理器)继续运行。
当 C1 对应的写事件就绪时,程序就会将保存在客户端结构回复缓存中的数据回写给客户端,当客户端接收到数据之后,它就将结果打印出来,显示给用户看。
以上就是 SET YEAR 2013
命令执行的整个过程。