UP | HOME

服务器与客户端

Table of Contents

服务器本身并没有多少需要介绍的新东西, 因为服务器除了维持服务器状态之外, 最重要的就是将各个功能模块组合起来。所以接下来将焦点放在服务器的初始化过程, 以及服务器对命令的处理过程上

  1. 首先介绍服务器的 初始化 操作, 观察一个 Redis 服务器从启动到可以接受客户端连接, 需要经过什么步骤
  2. 接着介绍客户端是如何 连接 到服务器的, 而服务器又是如何维持多个客户端的不同状态的
  3. 最后介绍命令从 发送处理 的整个过程, 并列举了一个 SET 命令的执行过程作为例子

初始化服务器

从启动 Redis 服务器, 到服务器可以接受外来客户端的网络连接这段时间, Redis 需要执行一系列初始化操作。整个初始化过程可以分为以下六个步骤:

  1. 初始化服务器全局状态
  2. 载入配置文件
  3. 创建 daemon 进程
  4. 初始化服务器功能模块
  5. 载入数据
  6. 开始事件循环

以下将介绍 Redis 服务器初始化的各个步骤

初始化服务器全局状态

redis.h/redisServer 结构记录了和服务器相关的所有数据, 这个结构主要包含以下信息:

  • 服务器中的 所有数据库
  • 命令表 :在执行命令时,根据字符来查找相应命令的实现函数
  • 事件 状态
  • 服务器的 网络连接信息 :套接字地址、端口,以及套接字描述符
  • 所有 已连接客户端 的信息
  • Lua 脚本的运行环境 及相关选项
  • 实现 订阅与发布 pub/sub 功能所需的数据结构
  • 日志 log 和慢查询日志 slowlog 的选项和相关信息
  • 数据 持久化 AOFRDB 的配置和状态
  • 服务器配置选项
    • 要创建多少个数据库
    • 是否将服务器进程作为 daemon 进程来运行
    • 最大连接多少个客户端
    • 压缩结构 zip structure 的实体数量
    • 。。。。。。。
  • 统计信息:
    • 键有多少次命令不命中
    • 服务器的运行时间
    • 内存占用
    • 。。。。。。
为了简洁起见,上面只列出了单机情况下的 Redis 服务器信息,不包含 SENTINEL 、 MONITOR 、 CLUSTER 等功能的信息

在这一步, 程序 创建 一个 redisServer 结构的实例变量 server 用作服务器的 全局状态 , 并将 server 的各个属性初始化为 默认值

当 server 变量的初始化完成之后, 程序进入服务器初始化的下一步: 读入配置文件

载入配置文件

上一步中, 程序为 server 变量(也即是服务器状态)的各个属性设置了默认值, 但这些默认值有时候并不是最合适的:

  • 用户可能想使用 AOF 持久化,而不是默认的 RDB 持久化
  • 用户可能想用其他端口来运行 Redis ,以避免端口冲突
  • 用户可能不想使用默认的 16 个数据库,而是分配更多或更少数量的数据库
  • 用户可能想对默认的内存限制措施和回收策略做调整
  • 。。。。。。

为了让使用者按自己的要求配置服务器, Redis 允许用户在运行服务器时, 提供相应的 配置文件 config file 或者显式的 选项 option , Redis 在初始化完 server 变量之后, 会读入配置文件和选项, 然后根据这些配置来对 server 变量的属性值做相应的修改:

  1. 如果单纯执行 redis-server 命令,那么服务器以默认的配置来运行 Redis
  2. 另一方面, 如果给 Redis 服务器送入一个配置文件, 那么 Redis 将按配置文件的设置来更新服务器的状态

    比如说, 通过命令 redis-server /etc/my-redis.conf , Redis 会根据 my-redis.conf 文件的内容来对服务器状态做相应的修改
    
  3. 除此之外, 还可以显式地给服务器传入选项, 直接修改服务器配置

    举个例子, 通过命令 redis-server --port 10086 , 可以让 Redis 服务器端口变更为 10086
    
  4. 当然, 同时使用配置文件和显式选项也是可以的, 如果文件和选项有冲突的地方, 那么优先使用 选项所指定的配置值

    如果运行命令 redis-server /etc/my-redis.conf --port 10086 , 并且 my-redis.conf 也指定了 port 选项
    
    那么服务器将优先使用 --port 10086 (实际上是选项指定的值覆盖了配置文件中的值)
    

创建 daemon 进程

Redis 默认以 daemon 进程的方式运行。当服务器初始化进行到这一步时, 程序将 创建 daemon 进程 来运行 Redis , 并 创建 相应的 pid 文件

初始化服务器功能模块

在这一步, 初始化程序完成两件事:

  • server 变量子属性 分配 内存
  • 初始化 这些子属性

为数据结构分配内存, 并初始化这些数据结构, 等同于对相应的功能进行初始化

比如说, 当为订阅与发布所需的链表分配内存之后, 订阅与发布功能就处于就绪状态, 随时可以为 Redis 所用了

在这一步, 程序完成的主要动作如下:

  • 初始化 Redis 进程的 信号 功能
  • 初始化 日志 功能
  • 初始化 客户端 功能
  • 初始化 共享 对象
  • 初始化 事件 功能
  • 初始化 数据库
  • 初始化 网络连接
  • 初始化 订阅与发布 功能
  • 初始化各个 统计变量
  • 关联 服务器定时操作 (cron job)到 时间事件 ,关联 客户端应答处理器文件事件
  • 如果 AOF 功能已打开,那么打开或创建 AOF 文件
  • 设置 内存限制
  • 初始化 Lua 脚本环境
  • 初始化 慢查询功能
  • 初始化 后台定时线程

完成这一步之后, 服务器打印出 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

初始化完成之后, 服务器状态和各个模块之间的关系图:

server.png

客户端连接到服务器

当 Redis 服务器完成初始化之后, 它就准备好可以接受外来客户端的连接了

当一个客户端通过 套接字函数 connect 到服务器时, 服务器执行以下步骤:

  1. 服务器通过 文件事件 无阻塞accept 客户端连接,并返回一个 套接字描述符 fd
  2. 服务器为 fd 创建 一个对应的 redis.h/redisClient 结构 实例,并将该实例 加入 到服务器的 已连接客户端的链表
  3. 服务器在 事件处理器 为该 fd 关联 读文件事件
完成这三步之后,服务器就可以等待客户端发来命令请求了

Redis 以 多路复用 的方式来处理多个客户端, 为了让多个客户端之间 独立分开 、不互相干扰, 服务器为每个已连接客户端维持一个 redisClient 结构, 从而单独保存该客户端的状态信息。redisClient 结构主要包含以下信息:

  • 套接字描述符
  • 客户端正在使用的 数据库指针数据库号码
  • 客户端的 查询缓冲 query buffer回复缓存 reply buffer
  • 一个指向 命令函数指针 ,以及 字符串形式的命令、命令参数和命令个数,这些属性会在命令执行时使用
  • 客户端 状态 :记录了客户端是否处于 SLAVE , MONITOR 或者 事务 状态
  • 实现 事务 功能(比如 MULTI 和 _WATCH_)所需的数据结构
  • 实现 阻塞 功能(比如 BLPOPBRPOPLPUSH )所需的数据结构
  • 实现 订阅与发布 功能(比如 PUBLISHSUBSCRIBE )所需的数据结构
  • 统计 数据和选项:
    • 客户端创建的时间
    • 客户端和服务器最后交互的时间
    • 缓存的大小
    • 。。。。。。
为了简洁起见,上面列出的客户端结构信息不包含复制(replication)的相关属性。

命令的请求、处理和结果返回

当客户端连上服务器之后, 客户端就可以向服务器发送命令请求了

从客户端发送命令请求, 到命令被服务器处理、并将结果返回客户端, 整个过程有以下步骤:

  1. 客户端通过 套接字 向服务器 传送 命令协议数据
  2. 服务器通过 读事件 来处理传入数据,并将 数据 保存 在客户端对应 redisClient 结构查询缓存
  3. 根据客户端查询缓存中的内容,程序从 命令表查找 相应 命令的实现函数
  4. 程序执行命令的实现函数, 修改 服务器的 全局状态 server 变量 ,并将 命令的执行结果 保存 到 客户端 redisClient 结构回复缓存 中,然后为该 客户端的 fd 关联 写事件
  5. 当客户端 fd 的写事件 就绪 时,将回复缓存中的 命令结果 传回客户端 。至此,命令执行完毕

命令请求实例: SET 的执行过程

为了更直观地理解命令执行的整个过程, 用一个实际执行 SET 命令的例子来讲解命令执行的过程。

假设现在客户端 C1 是连接到服务器 S 的一个客户端, 当用户执行命令 SET YEAR 2013 时:

  1. 客户端调用 写入 send 函数 , 将协议内容 *3\r\n$3\r\nSET\r\n$4\r\nYEAR\r\n$4\r\n2013\r\n" 写入 连接到服务器的 套接字
  2. 当 S 的 文件事件处理器 执行时, 它会察觉到 C1 所对应的读事件 已经 就绪 , 于是它将协议文本读入, 并 保存查询缓存
  3. 通过对查询缓存进行 分析 parse , 服务器在 命令表查找 SET 字符串所对应的命令实现函数 , 最终定位到 t_string.c/setCommand 函数, 另外, 两个命令参数 YEAR2013 也会以 字符串的形式 保存客户端结构
  4. 接着, 程序将 客户端 ,要 执行的命令命令参数 等送入 命令执行器
    • 执行器调用 setCommand 函数 , 将数据库中 YEAR 键的值修改为 2013
    • 然后将 命令的执行结果 保存客户端的回复缓存
    • 并为 客户端 fd 关联 写事件 , 用于将结果回写给客户端

      因为 YEAR 键的修改, 其他和数据库命名空间相关程序, 比如 AOF 、REPLICATION 还有事务安全性检查(是否修改了被 WATCH 监视的键?)也会被触发
      
      当这些后续程序也执行完毕之后, 命令执行器退出, 服务器其他程序(比如时间事件处理器)继续运行
      
  5. C1 对应的写事件 就绪 时, 程序就会将保存在客户端结构 回复缓存中的数据 回写客户端 , 当客户端接收到数据之后, 它就将结果打印出来, 显示给用户看
以上就是 SET YEAR 2013 命令执行的整个过程

小结

  • 服务器经过初始化之后,才能开始接受命令
  • 服务器初始化可以分为六个步骤:
    1. 初始化服务器全局状态
    2. 载入配置文件
    3. 创建 daemon 进程
    4. 初始化服务器功能模块
    5. 载入数据
    6. 开始事件循环
  • 服务器为每个已连接的客户端维持一个客户端结构,这个结构保存了这个客户端的所有状态信息
  • 客户端向服务器发送命令,服务器接受命令然后将命令传给命令执行器,执行器执行给定命令的实现函数,执行完成之后,将结果保存在缓存,最后回传给客户端

    Next:数据库 Previous:事件 Home:内部机制