Redis学习(六)客户端/服务器

      Redis服务器是典型的一对多服务器程序:一个服务器与多个客户端建立网络连接,每个可互段可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。

      通过使用由I/O多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式处理命令请求,并与多个客户端进行网络通信。对于每个与服务器连接的客户端,服务器都为这些客户端建立了相应的redisClient结构(客户端状态),这个结构保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构。

目录

Redis客户端

客户端属性

客户端的创建与关闭

总结

Redis服务器

命令请求的执行过程

serverCron 函数

服务器初始化

总结


Redis客户端

客户端属性

客户端状态包含的属性可以分为两类:

  • 一类是比较通用的属性, 这些属性很少与特定功能相关, 无论客户端执行的是什么工作, 它们都要用到这些属性。
  • 另外一类是和特定功能相关的属性, 比如操作数据库时需要用到的 db 属性和 dictid 属性, 执行事务时需要用到的 mstate 属性, 以及执行 WATCH 命令时需要用到的 watched_keys 属性, 等等。

1、套接字描述符

typedef struct redisClient {
    // ...
    //套接字描述符
    int fd;

    // ...
} redisClient;

客户端状态的 fd 属性记录了客户端正在使用的套接字描述符,根据客户端类型的不同,fd 属性的值也不同:

  • 伪客户端(fake client)的 fd 属性的值为 -1 : 伪客户端处理的命令请求来源于 AOF 文件或者 Lua 脚本, 而不是网络, 所以这种客户端不需要套接字连接, 自然也不需要记录套接字描述符。 目前 Redis 服务器会在两个地方用到伪客户端, 一个用于载入 AOF 文件并还原数据库状态, 而另一个则用于执行 Lua 脚本中包含的 Redis 命令。
  • 普通客户端的 fd 属性的值为大于 -1 的整数: 普通客户端使用套接字来与服务器进行通讯,服务器会用 fd 属性来记录客户端套接字的描述符。 因为合法的套接字描述符不能是 -1 ,所以普通客户端的套接字描述符的值必然是大于 -1 的整数。

2、名字和标志

在默认情况下, 一个连接到服务器的客户端是没有名字的。使用 CLIENT_SETNAME 命令可以为客户端设置一个名字, 让客户端的身份变得更清晰。

客户端的名字记录在客户端状态的 name 属性里面,如果客户端没有为自己设置名字,那么相应客户端状态的 name 属性指向NULL指针; 如果客户端为自己设置了名字, 那么 name属性将指向一个字符串对象, 而该对象就保存着客户端的名字。

客户端的标志属性 flags 记录了客户端的角色(role),以及客户端目前所处的状态:

//flags 属性的值可以是单个标志
flags = <flag>

//也可以是多个标志的二进制
flags = <flag1> | <flag2> | ...

每个标志使用一个常量表示, 一部分标志记录了客户端的角色:

  • 在主从服务器进行复制操作时, 主服务器会成为从服务器的客户端, 而从服务器也会成为主服务器的客户端。 REDIS_MASTER 标志表示客户端代表的是一个主服务器, REDIS_SLAVE 标志表示客户端代表的是一个从服务器。
  • REDIS_PRE_PSYNC 标志表示客户端代表的是一个版本低于 Redis 2.8 的从服务器, 主服务器不能使用 PSYNC 命令与这个从服务器进行同步。 这个标志只能在 REDIS_SLAVE 标志处于打开状态时使用。
  • REDIS_LUA_CLIENT 标识表示客户端是专门用于处理 Lua 脚本里面包含的 Redis 命令的伪客户端。

而另外一部分标志则记录了客户端目前所处的状态:

  • REDIS_MONITOR 标志表示客户端正在执行 MONITOR 命令。
  • REDIS_UNIX_SOCKET 标志表示服务器使用 UNIX 套接字来连接客户端。
  • REDIS_BLOCKED 标志表示客户端正在被 BRPOP 、 BLPOP 等命令阻塞。
  • REDIS_UNBLOCKED 标志表示客户端已经从 REDIS_BLOCKED 标志所表示的阻塞状态中脱离出来, 不再阻塞。 REDIS_UNBLOCKED 标志只能在 REDIS_BLOCKED 标志已经打开的情况下使用。
  • REDIS_MULTI 标志表示客户端正在执行事务。
  • REDIS_DIRTY_CAS 标志表示事务使用 WATCH 命令监视的数据库键已经被修改, REDIS_DIRTY_EXEC 标志表示事务在命令入队时出现了错误, 以上两个标志都表示事务的安全性已经被破坏, 只要这两个标记中的任意一个被打开, EXEC 命令必然会执行失败。 这两个标志只能在客户端打开了 REDIS_MULTI 标志的情况下使用。
  • REDIS_CLOSE_ASAP 标志表示客户端的输出缓冲区大小超出了服务器允许的范围, 服务器会在下一次执行 serverCron 函数时关闭这个客户端, 以免服务器的稳定性受到这个客户端影响。 积存在输出缓冲区中的所有内容会直接被释放, 不会返回给客户端。
  • REDIS_CLOSE_AFTER_REPLY 标志表示有用户对这个客户端执行了 CLIENT_KILL 命令, 或者客户端发送给服务器的命令请求中包含了错误的协议内容。 服务器会将客户端积存在输出缓冲区中的所有内容发送给客户端, 然后关闭客户端。
  • REDIS_ASKING 标志表示客户端向集群节点(运行在集群模式下的服务器)发送了 ASKING 命令。
  • REDIS_FORCE_AOF 标志强制服务器将当前执行的命令写入到 AOF 文件里面, REDIS_FORCE_REPL 标志强制主服务器将当前执行的命令复制给所有从服务器。 执行 PUBSUB 命令会使客户端打开 REDIS_FORCE_AOF 标志, 执行 SCRIPT_LOAD 命令会使客户端打开 REDIS_FORCE_AOF标志和 REDIS_FORCE_REPL 标志。
  • 在主从服务器进行命令传播期间, 从服务器需要向主服务器发送 REPLICATION ACK 命令, 在发送这个命令之前, 从服务器必须打开主服务器对应的客户端的 REDIS_MASTER_FORCE_REPLY 标志, 否则发送操作会被拒绝执行。

 flags 属性的例子:

# 客户端是一个主服务器
REDIS_MASTER

# 客户端正在被列表命令阻塞
REDIS_BLOCKED

# 客户端正在执行事务,但事务的安全性已被破坏
REDIS_MULTI | REDIS_DIRTY_CAS

# 客户端是一个从服务器,并且版本低于 Redis 2.8
REDIS_SLAVE | REDIS_PRE_PSYNC

# 这是专门用于执行 Lua 脚本包含的 Redis 命令的伪客户端
# 它强制服务器将当前执行的命令写入 AOF 文件,并复制给从服务器
REDIS_LUA_CLIENT | REDIS_FORCE_AOF | REDIS_FORCE_REPL

3、输入缓冲区

客户端状态的输入缓冲区用于保存客户端发送的命令请求:

typedef struct redisClient {
    // ...

    sds querybuf;

    // ...
} redisClient;

输入缓冲区的大小会根据输入内容动态地缩小或者扩大, 但它的最大大小不能超过 1 GB , 否则服务器将关闭这个客户端。

在服务器将客户端发送的命令请求保存到客户端状态的 querybuf 属性之后, 服务器将对命令请求的内容进行分析, 并将得出的命令参数以及命令参数的个数分别保存到客户端状态的 argv 属性和 argc 属性:

  • argv 属性是一个数组,数组中的每个项都是一个字符串对象:其中 argv[0] 是要执行的命令,而之后的其他项则是传给命令的参数。
  • argc 属性则负责记录 argv 数组的长度。

                            digraph {      label = "\n å¾ 13-5    argv å±æ§å argc å±æ§ç¤ºä¾";      rankdir = LR;      node [shape = record];      redisClient [label = " redisClient | ... | <argv> argv | argc \n 3 | ... ", width = 2];      argv [label = " { { <head> argv[0] | StringObject \n \"SET\" } | { argv[1] | StringObject \n \"key\" } | { argv[2] | StringObject \n \"value\" } } "];      redisClient:argv -> argv:head;  }

4、命令实现

当服务器从协议内容中分析并得出 argv 属性和 argc 属性的值之后, 服务器将根据项 argv[0] 的值, 在命令表中查找命令所对应的命令实现函数。

命令表是一个字典, 字典的键是一个 SDS 结构, 保存了命令的名字, 字典的值是命令所对应的 redisCommand 结构, 这个结构保存了命令的实现函数、 命令的标志、 命令应该给定的参数个数、 命令的总执行次数和总消耗时长等统计信息。

digraph {      label = "\n å¾ 13-6    å½ä»¤è¡¨";      rankdir = LR;      node [shape = record];      command_table [label = " dict | ... | <set> \"set\" | ... | <get> \"get\" | ... | <rpush> \"rpush\" | ... ", width = 1.5 ];      node [label = " <head> redisCommand | ... "];      command_table:set -> set:head;     command_table:get -> get:head;     command_table:rpush -> rpush:head;  }

当程序在命令表中成功找到 argv[0] 所对应的 redisCommand 结构时, 它会将客户端状态的 cmd 指针指向这个结构:

typedef struct redisClient {
    // ...

    struct redisCommand *cmd;

    // ...
} redisClient;

之后, 服务器就可以使用 cmd 属性所指向的 redisCommand 结构, 以及 argv 、 argc 属性中保存的命令参数信息, 调用命令实现函数, 执行客户端指定的命令。

                digraph {      label = "\n å¾ 13-7    æ¥æ¾å½ä»¤å¹¶è®¾ç½® cmd å±æ§";      rankdir = LR;      node [shape = record];      command_table [label = " dict | ... | <set> \"set\" | ... | <get> \"get\" | ... | <rpush> \"rpush\" | ... ", width = 1.5 ];      node [label = " <head> redisCommand | ... "];      command_table:set -> set:head [style = dashed];     command_table:get -> get:head;     command_table:rpush -> rpush:head;      redisClient [label = " redisClient | ... | <cmd> cmd | ... "];      set:head -> redisClient:cmd [dir = back, label = "2) \n 设置 \n cmd \n å±æ§"];      find [label = "1) \n æ¥æ¾ \n \"SET\" \n 对åºç\n redisCommand \n ç»æ", shape = plaintext];      find -> command_table:set [style = dashed];  }

5、输出缓冲区

执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面, 每个客户端都有两个输出缓冲区可用, 一个缓冲区的大小是固定的, 另一个缓冲区的大小是可变的:

  • 固定大小的缓冲区用于保存那些长度比较小的回复, 比如 OK 、简短的字符串值、整数值、错误回复,等等。
  • 可变大小的缓冲区用于保存那些长度比较大的回复, 比如一个非常长的字符串值, 一个由很多项组成的列表, 一个包含了很多元素的集合, 等等。

客户端的固定大小缓冲区由 buf 和 bufpos 两个属性组成:

typedef struct redisClient {
    // ...

    char buf[REDIS_REPLY_CHUNK_BYTES];

    int bufpos;

    // ...
} redisClient;

buf 是一个大小为 REDIS_REPLY_CHUNK_BYTES 字节的字节数组, 而 bufpos 属性则记录了 buf 数组目前已使用的字节数量

REDIS_REPLY_CHUNK_BYTES 常量目前的默认值为 16*1024 , 也即是说, buf 数组的默认大小为 16 KB 。

可变大小缓冲区由 reply 链表和一个或多个字符串对象组成:

digraph {      label = "\n å¾ 13-9    å¯å大å°ç¼å²åºç¤ºä¾";      rankdir = LR;      node [shape = record];      redisClient [label = " redisClient | ... | <reply> reply | ... ", width = 2];      node [label = " <head> StringObject \n ... "];      redisClient:reply -> s1:head -> s2:head -> s3:head;  }

6、身份验证

客户端状态的 authenticated 属性用于记录客户端是否通过了身份验证:

typedef struct redisClient {
    // ...

    int authenticated;

    // ...
} redisClient;

如果 authenticated 的值为 0 , 那么表示客户端未通过身份验证; 如果 authenticated 的值为 1 , 那么表示客户端已经通过了身份验证。

当客户端 authenticated 属性的值为 0 时, 除了 AUTH 命令之外, 客户端发送的所有其他命令都会被服务器拒绝执行,当客户端通过 AUTH 命令成功进行身份验证之后, 客户端状态 authenticated 属性的值就会从 0 变为 1 。

authenticated 属性仅在服务器启用了身份验证功能时使用: 如果服务器没有启用身份验证功能的话, 那么即使authenticated 属性的值为 0(这是默认值), 服务器也不会拒绝执行客户端发送的命令请求。

客户端的创建与关闭

创建普通客户端

如果客户端是通过网络连接与服务器进行连接的普通客户端,那么客户端使用 connect 函数连接服务器时,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾。

关闭普通客户端

客户端关闭有下述情况:

  • 客户端进程退出,或被kill,或成为client kill的目标。
  • 客户端发送不符合协议格式的请求,或客户端发送命令超出默认的1GB大小的请求,或服务器回复给客户端的内容超过输出缓冲区规定大小的内容。
  • 用户设置空转时限而客户端达到此时限(该条件有例外,如客户端是主服务器、从服务器、客户端被阻塞、客户端正在发布订阅,就算超过时限也不会被关闭)。

服务器通过两种方式来先知客户端输出缓冲区的大小:

  • 硬性限制:规定一个值,输出缓冲区超过这个值,立刻关闭该客户端。
  • 软性限制:当超过这个值,但是还没超过硬性限制,会写入redis客户端结构体中obuf_soft_limit_reached_time属性,作为超过软性限制开始的时间,之后服务器会持续监控此客户端,如果超出规定的软性限制的时间,则关闭客户端;如果软性限制时间之前,客户端输出缓冲区内容减小到软性限制之前,则不关闭客户端并且将obuf_soft_limit_reached_time的值清零。

伪客户端

Lua伪客户端,不是存放在redisServer的clients链表中,而是单独有一个redisClient类型的属性lua_client,用于存放redis的lua伪客户端。该客户端创建后,不会关闭,直到服务器关闭才会关闭。

AOF伪客户端,服务器载入aof文件时,会创建aof伪客户端,并且载入完毕后关闭该客户端。

总结

  • 服务器状态结构使用 clients 链表连接起多个客户端状态, 新添加的客户端状态会被放到链表的末尾。
  • 客户端状态的 flags 属性使用不同标志来表示客户端的角色, 以及客户端当前所处的状态。
  • 输入缓冲区记录了客户端发送的命令请求, 这个缓冲区的大小不能超过 1 GB 。
  • 命令的参数和参数个数会被记录在客户端状态的 argv 和 argc 属性里面, 而 cmd 属性则记录了客户端要执行命令的实现函数。
  • 客户端有固定大小缓冲区和可变大小缓冲区两种缓冲区可用, 其中固定大小缓冲区的最大大小为 16 KB , 而可变大小缓冲区的最大大小不能超过服务器设置的硬性限制值。
  • 输出缓冲区限制值有两种, 如果输出缓冲区的大小超过了服务器设置的硬性限制, 那么客户端会被立即关闭; 除此之外, 如果客户端在一定时间内, 一直超过服务器设置的软性限制, 那么客户端也会被关闭。
  • 当一个客户端通过网络连接连上服务器时, 服务器会为这个客户端创建相应的客户端状态。 网络连接关闭、 发送了不合协议格式的命令请求、 成为 CLIENT_KILL 命令的目标、 空转时间超时、 输出缓冲区的大小超出限制, 以上这些原因都会造成客户端被关闭。
  • 处理 Lua 脚本的伪客户端在服务器初始化时创建, 这个客户端会一直存在, 直到服务器关闭。
  • 载入 AOF 文件时使用的伪客户端在载入工作开始时动态创建, 载入工作完毕之后关闭。

Redis服务器

Redis服务器负责与多个客户端建立网络连接,处理客户端发来的命令请求,在数据库中保存客户端执行命令所产生的数据并通过资源管理来维持服务器自身的运转。

命令请求的执行过程

一个命令请求从发送到获得回复的过程中, 客户端和服务器需要完成一系列操作。

举个例子, 如果我们使用客户端执行以下命令:     SET KEY VALUE

那么从客户端发送 SET KEY VALUE 命令到获得回复 OK 期间, 客户端和服务器共需要执行以下操作:

  1. 客户端向服务器发送命令请求 SET KEY VALUE 。
  2. 服务器接收并处理客户端发来的命令请求 SET KEY VALUE , 在数据库中进行设置操作, 并产生命令回复 OK 。
  3. 服务器将命令回复 OK 发送给客户端。
  4. 客户端接收服务器返回的命令回复 OK , 并将这个回复打印给用户观看。

1、发送命令请求

Redis 服务器的命令请求来自 Redis 客户端, 当用户在客户端中键入一个命令请求时, 客户端会将这个命令请求转换成协议格式, 然后通过连接到服务器的套接字, 将协议格式的命令请求发送给服务器。

          digraph {      label = "\n\n å¾ 14-1    客æ·ç«¯æ¥æ¶å¹¶åéå½ä»¤è¯·æ±çè¿ç¨";      rankdir = LR;      node [shape = plaintext];      user [label = "ç¨æ·"];      client [label = "客æ·ç«¯"];      server [label = "æå¡å¨"];      //      user -> client [label = "é®å¥å½ä»¤è¯·æ±"];      client -> server [label = "å°å½ä»¤è¯·æ±è½¬æ¢æå议格å¼\nç¶ååé"];  }

2、读取命令请求

当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时, 服务器将调用命令请求处理器来执行以下操作:

  1. 读取套接字中协议格式的命令请求, 并将其保存到客户端状态的输入缓冲区里面。
  2. 对输入缓冲区中的命令请求进行分析, 提取出命令请求中包含的命令参数, 以及命令参数的个数, 然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里面。
  3. 调用命令执行器, 执行客户端指定的命令。

3、命令执行器

(1)查找

命令执行器要做的第一件事就是根据客户端状态的 argv[0] 参数,在命令表(command table)中查找参数所指定的命令, 并将找到的命令保存到客户端状态的 cmd 属性里面。

属性名类型作用
namechar *命令的名字,比如 "set" 。
procredisCommandProc *函数指针,指向命令的实现函数,比如 setCommand 。 redisCommandProc 类型的定义为typedef void redisCommandProc(redisClient *c); 。
arityint命令参数的个数,用于检查命令请求的格式是否正确。 如果这个值为负数 -N ,那么表示参数的数量大于等于 N 。 注意命令的名字本身也是一个参数, 比如说 SET msg "helloworld" 命令的参数是 "SET" 、 "msg" 、 "hello world" , 而不仅仅是 "msg" 和 "hello world"
sflagschar *字符串形式的标识值, 这个值记录了命令的属性, 比如这个命令是写命令还是读命令, 这个命令是否允许在载入数据时使用, 这个命令是否允许在 Lua 脚本中使用, 等等。
flagsint对 sflags 标识进行分析得出的二进制标识, 由程序自动生成。 服务器对命令标识进行检查时使用的都是 flags 属性而不是 sflags 属性, 因为对二进制标识的检查可以方便地通过 & 、 ^ 、 ~ 等操作来完成。
callslong long服务器总共执行了多少次这个命令。
millisecondslong long服务器执行这个命令所耗费的总时长。

digraph {      label = "\n å¾ 14-4    å½ä»¤è¡¨";      rankdir = LR;      node [shape = record];      commands [label = " å½ä»¤è¡¨ | ... | <set> \"set\" | <get> \"get\" | ... | <sadd> \"sadd\" | ... | <rpush> \"rpush\" | ... | <publish> \"publish\" | ... ", width = 2.0];      set [label = " <head> redisCommand | name \n \"set\" | <proc> proc | arity \n -3 | sflags \n \"wm\" | ... "];     get [label = " <head> redisCommand | name \n \"get\" | <proc> proc | arity \n 2 | sflags \n \"r\" | ... "];     //sadd [label = " <head> redisCommand | name \n \"sadd\" | <proc> proc | arity \n -3 | sflags \n \"wm\" | ... "];     //rpush [label = " <head> redisCommand | name \n \"rpush\" | <proc> proc | arity \n -3 | sflags \n \"wm\" | ... "];     //publish [label = " <head> redisCommand | name \n \"publish\" | <proc> proc | arity \n 3 | sflags \n \"pltr\" | ... "];      node [shape = plaintext];      setCommand [label = "void setCommand(redisClient *c);"];     getCommand [label = "void getCommand(redisClient *c);"];     //saddCommand;     //rpushCommand;     //publishCommand;      //      commands:set -> set:head; set:proc -> setCommand;     commands:get -> get:head; get:proc -> getCommand;     //commands:sadd -> sadd:head; sadd:proc -> saddCommand;     //commands:rpush -> rpush:head; rpush:proc -> rpushCommand;     //commands:publish -> publish:head; publish:proc -> publishCommand;      //* fix editor highlight  }

digraph {      label = "\n å¾ 14-5    设置客æ·ç«¯ç¶æç cmd æé";      rankdir = LR;      node [shape = record];      redisClient [label = " redisClient | ... | <cmd> cmd | ... ", width = 2];      commands [label = " å½ä»¤è¡¨ | ... | <set> \"set\" | <get> \"get\" | ... | <sadd> \"sadd\" | ... | <rpush> \"rpush\" | ... | <publish> \"publish\" | ... ", width = 2.0];      set [label = " <head> redisCommand | name \n \"set\" | <proc> proc | arity \n -3 | sflags \n \"wm\" | ... "];      node [shape = plaintext];      setCommand [label = "void setCommand(redisClient *c);"];     //* fix editor highlight      //      redisClient:cmd -> set:head [label = "æå \n \"set\" é®å¯¹åºç \n redisCommand \n ç»æ"];      commands:set -> set:head; set:proc -> setCommand;  }

需要注意的是,因为命令表使用的是大小写无关的查找算法,所以命令名字的大小写不影响命令表的查找结果。

(2)执行预备

到目前为止, 服务器已经将执行命令所需的命令实现函数(保存在客户端状态的 cmd 属性)、参数(保存在客户端状态的 argv 属性)、参数个数(保存在客户端状态的 argc 属性)都收集齐了, 但是在真正执行命令之前, 程序还需要进行一些预备操作, 从而确保命令可以正确、顺利地被执行, 这些操作包括:

  • 检查客户端状态的 cmd 指针是否指向 NULL , 如果是的话, 那么说明用户输入的命令名字找不到相应的命令实现, 服务器不再执行后续步骤, 并向客户端返回一个错误。
  • 根据客户端 cmd 属性指向的 redisCommand 结构的 arity 属性, 检查命令请求所给定的参数个数是否正确, 当参数个数不正确时, 不再执行后续步骤, 直接向客户端返回一个错误。 比如说, 如果 redisCommand 结构的 arity 属性的值为 -3 , 那么用户输入的命令参数个数必须大于等于 3 个才行。
  • 检查客户端是否已经通过了身份验证, 未通过身份验证的客户端只能执行 AUTH 命令, 如果未通过身份验证的客户端试图执行除 AUTH 命令之外的其他命令, 那么服务器将向客户端返回一个错误。
  • 如果服务器打开了 maxmemory 功能, 那么在执行命令之前, 先检查服务器的内存占用情况, 并在有需要时进行内存回收, 从而使得接下来的命令可以顺利执行。 如果内存回收失败, 那么不再执行后续步骤, 向客户端返回一个错误。
  • 如果服务器上一次执行 BGSAVE 命令时出错, 并且服务器打开了 stop-writes-on-bgsave-error 功能, 而且服务器即将要执行的命令是一个写命令, 那么服务器将拒绝执行这个命令, 并向客户端返回一个错误。
  • 如果客户端当前正在用 SUBSCRIBE 命令订阅频道, 或者正在用 PSUBSCRIBE 命令订阅模式, 那么服务器只会执行客户端发来的 SUBSCRIBE 、 PSUBSCRIBE 、 UNSUBSCRIBE 、 PUNSUBSCRIBE 四个命令, 其他别的命令都会被服务器拒绝。
  • 如果服务器正在进行数据载入, 那么客户端发送的命令必须带有 l 标识(比如 INFO 、 SHUTDOWN 、 PUBLISH ,等等)才会被服务器执行, 其他别的命令都会被服务器拒绝。
  • 如果服务器因为执行 Lua 脚本而超时并进入阻塞状态, 那么服务器只会执行客户端发来的 SHUTDOWN nosave 命令和 SCRIPT KILL 命令, 其他别的命令都会被服务器拒绝。
  • 如果客户端正在执行事务, 那么服务器只会执行客户端发来的 EXEC 、 DISCARD 、 MULTI 、 WATCH 四个命令, 其他命令都会被放进事务队列中。
  • 如果服务器打开了监视器功能, 那么服务器会将要执行的命令和参数等信息发送给监视器。
  • 以上只列出了服务器在单机模式下执行命令时的检查操作, 当服务器在复制或者集群模式下执行命令时, 预备操作还会更多一些。

(3)调用命令的实现函数

在前面的操作中, 服务器已经将要执行命令的实现保存到了客户端状态的 cmd 属性里面, 并将命令的参数和参数个数分别保存到了客户端状态的 argv 属性和 argc 属性里面, 当服务器决定要执行命令时, 它只要执行以下语句就可以了:

// client 是指向客户端状态的指针
client->cmd->proc(client);

因为执行命令所需的实际参数都已经保存到客户端状态的 argv 属性里面了, 所以命令的实现函数只需要一个指向客户端状态的指针作为参数即可。

digraph {      label = "\n å¾ 14-6    客æ·ç«¯ç¶æ";      //      rankdir = LR;      node [shape = record];      redisClient [label = " redisClient | ... | <cmd> cmd | <argv> argv | argc \n 3 | ... ", width = 2];      set [label = " <head> redisCommand | name \n \"set\" | <proc> proc | arity \n -3 | sflags \n \"wm\" | ... "];      setCommand [label = "void setCommand(redisClient *c);", shape = plaintext];     //* fix editor highlight      redisClient:cmd -> set:head; set:proc -> setCommand;      argv [label = " { { <head> argv[0] | StringObject \n \"SET\" } | { argv[1] | StringObject \n \"KEY\" } | { argv[2] | StringObject \n \"VALUE\" } } "];      redisClient:argv -> argv:head;  }

被调用的命令实现函数会执行指定的操作, 并产生相应的命令回复, 这些回复会被保存在客户端状态的输出缓冲区里面(buf 属性和 reply属性), 之后实现函数还会为客户端的套接字关联命令回复处理器, 这个处理器负责将命令回复返回给客户端。

(4)后续工作

在执行完实现函数之后, 服务器还需要执行一些后续工作:

  • 如果服务器开启了慢查询日志功能, 那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。
  • 根据刚刚执行命令所耗费的时长, 更新被执行命令的 redisCommand 结构的 milliseconds 属性, 并将命令的 redisCommand 结构的 calls 计数器的值增一。
  • 如果服务器开启了 AOF 持久化功能, 那么 AOF 持久化模块会将刚刚执行的命令请求写入到 AOF 缓冲区里面。
  • 如果有其他从服务器正在复制当前这个服务器, 那么服务器会将刚刚执行的命令传播给所有从服务器。

当以上操作都执行完了之后, 服务器对于当前命令的执行到此就告一段落了, 之后服务器就可以继续从文件事件处理器中取出并处理下一个命令请求了

4、将命令回复返回客户端

前面说过, 命令实现函数会将命令回复保存到客户端状态的输出缓冲区里面,并为客户端的套接字关联命令回复处理器,当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。

当命令回复发送完毕之后, 回复处理器会清空客户端状态的输出缓冲区, 为处理下一个命令请求做好准备。

当客户端接收到协议格式的命令回复之后, 它会将这些回复转换成人类可读的格式, 并打印给用户观看。

digraph {      label = "\n\n å¾ 14-8    客æ·ç«¯æ¥æ¶å¹¶æå°å½ä»¤åå¤çè¿ç¨";      rankdir = LR;      node [shape = plaintext];      user [label = "ç¨æ·"];      client [label = "客æ·ç«¯"];      server [label = "æå¡å¨"];      //      server -> client [label = "åå¤å¤çå¨å°å议格å¼ç\nå½ä»¤åå¤è¿åç»å®¢æ·ç«¯"];      client -> user [label = "å°åå¤æ ¼å¼åæ人类å¯è¯»æ ¼å¼ \n ç¶åæå°æ¾ç¤º"];  }

serverCron 函数

Redis服务器中,默认情况下,serverCron函数每100毫秒执行一次,这个执行间隔可以在配置文件进行设置。这个函数是用于管理服务器的资源,保证服务器更良好的运转。

1、更新服务器时间缓存

redis中有许多功能要获取系统当前时间,则需要调用系统接口查询时间,这样比较耗时,因此redis在结构体中用unixtime、mstime属性,保存了当前时间,并且定时更新这个值。前者是秒级unix时间戳,后者是毫秒级unix时间戳

  • 但是,由于每100毫秒才更新因此,因而这两个值只会用在打印日志、更新服务器LRU时钟、决定是否进行持久化任务、计算服务器上线时间等,精度要求不高的地方使用。
  • 对于键过期时间、慢查询日志等,服务器会再次进行系统时间调用,获取最精确的时间。

2、更新lru时间

lru记录的是服务器最后一次被访问的时间,是用于服务器的计算空转时长,用属性lruclock进行存储。默认情况下,每10秒更新一次。另外,每个redis对象也存了一个lru,保存的是该对象最后一次被被访问的时间

当要计算redis对象的空转时间,则会用服务器的lru减去redis对象的lru,获得的结果即对象的空转时长。

在redis客户端,用命令objectidletime key,可以查看该key的空转时长,返回结果是以秒为单位。由于redis每10秒更新一次服务器的最后访问时间,因此不是很精确。

3、更新服务器每秒执行命令数

这个不是通过扫描全部的键,而是采用抽样的方式确定的结果。每100毫秒1次,随机抽取一些键,查看最近1秒是否有操作,来确定最近1秒的操作次数

接着,会将这个值,与上一次的结果,取平均值,作为本次计算的每秒执行命令数。在存入结构体中,供下次取平均值使用。

4、更新服务器内存峰值

redis服务器中,用stat_peak_memory记录服务器内存峰值。每次执行serverCron函数,会查看当前内存使用量,并且与stat_peak_memory比较,如果超过这个值,就更新这个属性。

5、处理sigterm信号

redis服务器,用属性shutdown_asap记录当前的结果,0是不用进行操作,1的话是要求服务器尽快关闭。

因此,服务器关闭命令shutdown执行,并不会立即关闭服务器,而是将服务器的shutdown_asap属性置成1,当下一次serverCron读取时,就会拒绝新的请求,完成当前正在执行的命令后,开始持久化相关的操作,结束持久化后才会关闭服务器。

6、管理客户端资源

主要是会检查客户端的以下内容:

  1. 客户端很长时间没有和服务器响应,服务器认为该客户端超时,则会断开和该客户端的连接。
  2. 当客户端在上一次执行命令请求后,输入缓冲区超过规定的长度,程序会释放输入缓冲区,并创建一个默认大小的缓冲区,防止缓冲区过分消耗。
  3. 关闭输出缓冲区超出大小限制的客户端。

7、管理数据库资源

主要是检查键是否过期,并且按照配置的策略,删除过期的键。如懒惰删除、定期删除等。

8、执行被延迟的bgrewriteaof命令

redis用属性aof_rewrite_scheduled记录是否有延迟的bgrewriteaof命令。

当执行bgsave命令期间,如果接收到bgrewriteaof命令,不会立即执行该命令,而是会将属性aof_rewrite_scheduled置成1。

每次执行serverCron函数执行时,发现属性aof_rewrite_scheduled是1,会检查当前是否在执行bgsave命令或bgrewriteaof命令,如果没有在执行这两个命令,则会执行bgrewriteaof命令。

9、检查持久化操作的运行状态

redis服务器分别用rdb_child_pidaof_child_pid属性,记录RDBAOF的子进程号(即子进程pid),如果没有在执行相应的持久化,则值是-1。

有一个值不是-1时:每次服务器检查这两个属性,发现有一个不是-1,则会检查子进程是否有信号发来服务器进程。

如果有信号,表示rdb完成或aof重写完毕,服务器会进行后续的操作,比如用新的rdb、aof替换旧的相应文件。

如果没信号,表示持久化还没完成,程序不做动作。

两个值都是-1时:两个值都不是-1,会进行三个检查:

  1. 如果bgrewriteaof命令有存在延迟(即上述aof_rewrite_scheduled值是1),因为两个属性都是 -1,表示当前没有在持久化,则redis服务器会开始aof的重写。
  2. 检查服务器是否满足bgsave条件,如果满足,因为两个属性都是 -1,则会开始执行bgsave。
  3. 检查服务器是否满足bgrewriteaof条件,如果满足,因为两个属性都是 -1,则会开始执行bgrewriteaof。

10、将aof缓冲区内容写入AOF文件

如果开启aof,redis会记录每个写命令,写入aof缓冲区,但是为了减少磁盘I/O,不会立即写入aof文件。而是在执行serverCron函数时,才会开始将缓冲区内容写入aof文件。

11、记录执行一次serverCron

redis用属性cronloops保存serverCron函数执行的次数。当执行一次serverCron,则会将属性值加1。

这个值目前的作用,是在主从复制情况下,会有一个条件是,每执行n次serverCron,则执行一次指定代码。

服务器初始化

redis服务器开启时,会先进行初始化,主要有五个步骤。

1、初始化状态结构

首先,会创建一个struct   redisServer实例变量,存储服务器的状态,并为结构中的各个属性设置默认值。

接着,redis初始化服务器,会执行一次redis.c/initServerConfig函数,主要工作是设置服务器运行ID、默认运行频率、默认配置文件路径、运行架构、默认端口号、RDB条件、AOF条件、LRU时钟、创建命令表

初始化状态结构,都是简单的结构,后续的数据库、共享对象、慢查询日志、Lua环境等,都是后面才创建的。

2、载入配置选项

在启动redis服务器时,可以通过给定配置参数指定配置文件来修改服务器的默认配置。redis会载入这些配置,并且在和默认不同的时候,会覆盖默认的配置。

例如输入redis-server –port 5000,则会先创建端口基于6379的,再在这一步修改端口号为5000。

在加载用户配置的文件,如果有定义新的结果,则使用新结果,否则就使用默认值。

3、初始化服务器数据结构

1)创建数据结构

在第一步,只创建了一个命令表,在此步骤则会创建其他数据结构,

server.client    //链表,用于存储普通客户端,每个节点是一个redisClient结构;
server.db        //链表,保存所有的数据库;
server.pubsub_channels//链表,保存频道订阅信息;server.pubsub_patterns链表,保存模式订阅信息。
server.lua       //用于执行lua脚本的环境。
server.showlog   //用于保存慢查询。

服务器会为上述结构分配内存空间。在此步骤才创建数据结构,是因为如果第一步创建,而第二步加载用户自定义配置的时候,有可能会修改到某些内容,则还需要重写。而命令表由于是固定的,因此可以放到第一步创建。

2)其他设置操作

除了创建数据结构,还会进行一些重要的设置。包括:

  • 为服务器设置进程信号处理器。
  • 创建共享对象,包括整数1~10000的字符串对象,“OK”、“ERR”回复的字符串对象等,用于避免反复创建相同对象。
  • 打开服务器监听端口,为监听的套接字添加相应的应答事件,等待服务器正式运行时接收客户端的连接。
  • 为serverCron函数创建时间事件,等待服务器正式执行serverCron。
  • 如果AOF持久化开启,则打开aof文件,如果不存在则创建aof文件。
  • 初始化服务器后台I/O模块(bio),为将来的I/O做好准备。

4、还原数据库状态

如果开启aof,则载入aof文件;如果没有开启aof,则载入rdb文件。

载入完成后,在日志中打印载入的耗时。

5、执行事件循环

初始化最后一步,服务器将打印连接成功的日志。并且开始事件循环,初始化正式完成,可以开始处理客户端的请求。

总结

  • 一个命令请求从发送到完成主要包括以下步骤: 1. 客户端将命令请求发送给服务器; 2. 服务器读取命令请求,并分析出命令参数; 3. 命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复; 4. 服务器将命令回复返回给客户端。
  • serverCron 函数默认每隔 100 毫秒执行一次, 它的工作主要包括更新服务器状态信息, 处理服务器接收的 SIGTERM 信号, 管理客户端资源和数据库状态, 检查并执行持久化操作, 等等。
  • 服务器从启动到能够处理客户端的命令请求需要执行以下步骤: 1. 初始化服务器状态; 2. 载入服务器配置; 3. 初始化服务器数据结构; 4. 还原数据库状态; 5. 执行事件循环。

 

 

 

参考文章:

《Redis设计与实现》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值