Redis客户端

Redis 是一个客户端服务端的程序,服务端提供数据存储等等服务,客户端连接服务端并通过向服务端发送命令,读取或写入数据,简单来说,客户端就是某种工具,我们通过它与 Redis 服务端进行通讯并完成数据操作。

客户端并不是 Redis 的核心,Redis 的核心是它的服务端程序,服务端程序才是完成数据存、取,持久化等等我们使用频繁的各种操作的执行者。但也不是说客户端就没什么作用,客户端在整个 Redis 服务体系中也是非常重要的一环。

Redis服务器是一个典型的一对多服务器程序:一个服务器可以与多个客户端建立网络连接,每个客户端可以向服务器发送命令请求,而服务器则接受并处理客户端发送的命令请求,并向客户端返回命令回复。通过使用由I/O多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。

服务器状态结构的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构,对客户端执行批量操作,或者查找某个指定的客户端,都可以通过遍历clients链表来完成:

struct redisServer{
    //,.,
    // 一个链表,保存了所有客户端状态
    list * clients;
}

 

 

一、客户端的基本属性

redis 中为客户端抽象的数据结构是,server.h/client 结构,我这里是 redis-4.0.x 版本,不同版本或许稍有不同,每一个 redis 客户端成功的连接上服务端之后,服务端就会创建一个 client 结构实例,并以链表的形式链接所有连接成功的客户端。

这个结构最主要作用就是存储当前客户端的大量属性,套接字、名字、标志,状态等等信息,这些信息非常的重要,当服务端为客户端服务时,很多的信息例如当前要执行的命令、参数都会从这里获取。我们一个一个来了解。

1、客户端名称

默认情况下,所有连接成功的客户端都是没有名字的,这一点你可以通过向服务发送 client list 命令验证,它会返回当前服务端成功建立的客户端以及他们的基本信息。例如:

可以看到,name 字段默认是空,如果你想让你的客户端辨识度更高,你可以向服务端发送 client setname 为你的客户端命名,这里我就不做演示了,客户端名称这个信息保存在 client 结构中的 name 字段里。

typedef struct client {
    .........
    robj *name;             /* As set by CLIENT SETNAME. */           
    .........
} client;

2、套接字

客户端套接字由客户端状态的 fd 属性记录

当 fd 属性值为-1 时,表示这个客户端是伪客户端。伪客户端的请求命令不是来源于网络的,而是来源于 Lua 脚本或 AOF 文件(后续详细介绍)的,所以伪客户端不需要套接字连接,它也没有套接字描述符。当我们执行的 Lua 脚本中含有 Redis 命令,或者使用 AOF 文件来还原数据库状态时,就会用到伪客户端。

当 fd 属性值是大于-1 的整数时,表示这个客户端是普通客户端。普通客户端采用相关套接字来实现与服务器的通信,因此服务器会利用 fd 属性来记录客户端套接字的描述符。

3、标志属性

标志用于描述当前 redis 客户端的一些状态或者角色,对应的到数据结构中就是一个整型字段。

typedef struct client {
    .........
    int flags;              /* Client flags: CLIENT_* macros. */
    .........
} client;

 

Redis 中定义了很多的客户端标志,客户端的标志属性 flags 用来记录客户端的角色(Role)及客户端目前所处的状态。

flags 属性的取值可以是单个标志,也可以是多个二进制或的组合标志,具体如下。

单个标志:flags=<flag>

组合标志:flags=<flag1>|<flag2>|<flag3>|…

标志使用常量来表示。Redis 所具有的所有标志都定义在 redis.h 文件中。

记录客户端角色的标志有如下几个:

  • 在利用 Redis 主从服务器实现复制时,主从服务器会相互成为对方的客户端,也就是从服务器是主服务器的客户端,同时主服务器也是从服务器的客户端。Redis 使用REDIS_MASTER 标志来表示这个客户端是主服务器,而使用 REDIS_SLAVE 标志来表示另一个客户端是从服务器。
  • Redis 使用 REDIS_LUA_CLIENT 标志来表示该客户端是一个专门用于处理 Lua 脚本的伪客户端,它主要用于执行 Lua 脚本中包含的 Redis 命令。
  • Redis 使用 REDIS_PRE_PSYNC 标志来表示该客户端是一个低于 Redis 2.8 版本的从服务器,此时,对应的主服务器不能使用 PSYNC 命令实现与从服务器的数据同步。只有当 REDIS_SLAVE 标志处于打开状态时,才能使用 REDIS_PRE_PSYNC 标志。

记录客户端当前状态的标志有如下几个:

  •  REDIS_ASKING 标志表示客户端向运行在集群模式下的服务器节点发送了 ASKING 命令。
  •  REDIS_CLOSE_ASAP 标志表示客户端的输出缓冲区过大,超出了服务器所允许的范围。当服务器在下一次执行 serverCron 函数时,会关闭这个输出缓冲区过大的客户端,以此来保证服务器的稳定性不受这个客户端影响。在关闭的时候,存储在这个缓冲区中的数据也会被删除,并且不会给客户端返回任何信息。
  •  REDIS_CLOSE_AFTER_REPLY 标志表示客户端给服务器发送的命令请求中有错误的协议内容,或者用户在客户端中执行了 CLIENT kill 命令。此时服务器会将客户端输出缓冲区中存储的所有数据内容发送给客户端,然后关闭这个客户端。
  •  REDIS_DIRTY_CAS 标志表示事务使用 WATCH 命令监视的数据库键已经被修改。
  •  REDIS_DIRTY_EXEC 标志表示事务在命令入队时出现错误。

REDIS_DIRTY_CAS 和 REDIS_DIRTY_EXEC 标志的出现都表示 Redis 事务的安全性已被破坏。只要这两个标志中的任何一个被打开,EXEC 命令都会执行失败。而只有在客户端打开了 REDIS_MULTI 标志的情况下,才能使用这两个标志。

  •  REDIS_MULTI 标志表示客户端正处于执行事务的状态中。
  •  REDIS_MONITOR 标志表示客户端正处于执行 MONITOR 命令的状态中。
  •  REDIS_FORCE_AOF 标志表示让服务器将当前正在执行的命令强制写入 AOF 文件中。在执行 PUBSUB 命令时,会使客户端打开 REDIS_FORCE_AOF 标志。
  •  REDIS_FORCE_REPL 标志表示强制让主服务器将当前正在执行的命令复制给所有与它连接的从服务器。当执行 SCRIPT LOAD 命令时,会使客户端同时开启 REDIS_FORCE_AOF 和 REDIS_FORCE_REPL 标志。如果要实现主从服务器可以正确地载入 SCRIPT LOAD 命令指定的脚本,那么服务器必须使用 REDIS_FORCE_REPL 标志,让主服务器强制将 SCRIPT LOAD 命令分发给相应的从服务器。
  •  REDIS_UNIX_SOCKET 标志表示服务器连接客户端使用的是 UNIX 套接字。
  •  REDIS_BLOCKED 标志表示客户端正处于被 BRPOP、BLPOP 等命令阻塞的状态中。
  •  REDIS_UNBLOCKED 标志表示客户端不再阻塞,它从 REDIS_BLOCKED 标志的阻塞状态中脱离出来。只有在 REDIS_BLOCKED 标志被打开的情况下,才能使用 REDIS_UNBLOCKED 标志。
  •  REDIS_MASTER_FORCE_REPLY 标志:在主从服务器进行命令交互的过程中,从服务器需要向主服务器发送 REPLICATION ACK 命令。但是,在发送此命令之前,从服务器必须开启主服务器对应的客户端的 REDIS_MASTER_FORCE_REPLY 标志;否则主服务器会拒绝执行从服务器发送的 REPLCATION ACK 命令。

当然了,上面那个 flages 的值只是举了个例子,描述了当前客户端是一个主节点的 server(当进行主从节点复制的时候,主节点会作为客户端连接从节点发送 RDB 文件给客户端),又正在执行 MONITOR 命令。前者描述了客户端角色,后者描述客户端状态。

总而言之,redis 客户端 flags 字段可以描述当前客户端的角色,也可以记录当前客户端各种状态信息,是服务端了解客户端信息的一个非常重要的字段。

 

4、输入/输出缓冲区

redis 服务端收到客户端发来的命令请求需要很多步骤来处理和调用相关命令的实现,并最终将数据返回给客户端,那么输入缓冲区其实就是一小块内存,用于存储客户端发送过来的命令,包括参数,这块内存空间默认不能超过 1GB,否则 redis 服务端就会强制关闭与该客户端的连接。

typedef struct client {
    .........
    sds querybuf;           /* Buffer we use to accumulate client queries. */
    .........
} client;

querybuf 就是客户端缓冲区,它是一个 SDS 类型的字段,那么说明这是一个可以动态扩充输入缓冲区。

当然我们也可以通过 client list 看看当前客户端的的 querybuf 分配和使用情况。

其中 qbuf 和 qbuf-free 用于描述客户端输入缓冲区状态。我这里的这个没有写入过大的命令,所以这里的 querybuf 只分配了 32768 个字节。

ps:尽量不要使用过大的 KEY,这样会导致客户端 querybuf 占用过多内存,这样会导致 redis 服务端程序占用过高内存,如果超过 maxmemory 限制,会触发 KEY 的 LRU 淘汰或程序异常。

除此之外,redis 客户端还有一个输出缓冲区,用于缓存服务端响应的回复。

输出缓冲区有两种,一种是固定大小的,用于存储服务端简单的响应,例如:OK,错误信息等。还有一种是非固定长度的缓冲区,它的长度是可动态扩展的,用于存储一些较长的响应内容。

typedef struct client {
    .........
    /* Response buffer */
    int bufpos;
    char buf[PROTO_REPLY_CHUNK_BYTES];
    .........
} client;

PROTO_REPLY_CHUNK_BYTES 等于 16*1024,也就是默认固定输出缓冲区只有 16K,bufpos 记录当前固定缓冲区已经使用的字节数。

typedef struct client {
    .........
    list *reply;            /* List of reply objects to send to the client. */
    .........
} client;

 

动态缓冲区用链表实现,可以为我们返回较大的 key,例如一些 set、list 集合等等。我们可以通过 client list 命令查看输出缓冲区的使用情况。

image

obl 表示固定缓冲区长度,oll 代表动态缓冲区长度,omem 表示固定缓冲区和动态缓冲区总共占用了多少字节。

ps:输出缓冲区可以通过配置 client-output-buffer-limit 限制最大内存上限,同样如果滥用,一样会导致 redis 服务器内存飙升,建议尽量配置小一点的输出缓存区大小。

5、命令与命令参数

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

int argc; /* Num of arguments of current command. */
robj **argv; /* Arguments of current command. */

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

6、命令的实现函数

当服务器从协议内容中分析得出argv 和 argc属性后,服务器会根据项argv[0]的值,在命令表中查找命令对应的命令实现函数。如图,命令表实际上是个字典,键是SDS,保存命令的名字;
值是redisCommand结构,这个结构保存了命令的实现函数、命令的标志、命令应该给定的参数个数、命令的总执行次数和总耗时。当程序在命令表中成功找到argv[0]所对应的redisCommand结构时,它会将客户端状态的cmd指针指向这个结构。之后服务器可以使用cmd属性指向的redisCommand结构,以及argv、argc属性中的命令参数信息,调用命令实现函数,执行客户端指定的命令。

7、身份验证

客户端状态的authenticated 属性用于记录客户端是否通过了身份验证:如果为0,那么表示客户端未通过身份验证,如果值为1,那么表示客户端已经通过了身份验证。该属性只有在配置文件对requirepass 选项打开的情况下使用。

当 authenticated 属性值为 0 时,服务器除执行 AUTH 命令之外,将会拒绝执行客户端发送过来的其他所有命令。

8、时间

客户端还有几个和时间有关的属性:

  • ctime属性记录了创建客户端的时间,用来记录客户端与服务端已经连接了多少秒;
  • lastinteraction 属性记录了客户端与服务器最后一次进行互动的时间,这里的互动可以是客户端向服务端发送命令请求,也可以是服务端向客户端发送命令回复。 lastinteraction 属性可以计算出客户端的空转时间,也就是在进行最后一次交互之前过去了多少时间,单位为秒。CLIENT list 命令返回的 idle 域记录了这个时间。当 idle 的值为 0 时,表示空转时间为 0 秒。
  • obuf_soft_limit_reached_time属性记录了输出缓冲区第一次到达软性限制(soft limit)的时间,下面说这个属性的用途。

二、客户端的创建和关闭

2.1、创建普通客户端

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

2.2、关闭客户端

普通客户端被关闭的几种方式:

● 当客户端执行了 CLIENT kill 命令时,客户端会被关闭。

● 当客户端进程被杀死时,客户端将会断开与服务器的连接,从而客户端被关闭。

● 当客户端向服务器发送的命令是错误协议格式时,客户端会被关闭。

● 当客户端发送的命令请求的大小超过了输入缓冲区的限制大小时,客户端会被关闭。

● 当发送给客户端的命令执行后返回结果的大小超过了输出缓冲区的限制大小时,客户端也会被关闭。

● 当为服务器设置了 timeout 参数值,同时客户端的空转时间又超过了 timeout 参数值时,客户端将会被关闭。而如果这个客户端是主服务器,而从服务器被 BLPOP、BRPOP 等相关命令阻塞,或者从服务器正在执行与订阅发布相关的命令,此时就算客户端的空转时间超过了 timeout 参数值,这个客户端也不会被关闭。

服务器使用两种模式来限制客户端输出缓冲区的大小:

  • 硬性限制(hard limit):如果输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器立即关闭客户端。
  • 软性限制(soft limit):如果输出缓冲区的大小超过了软性限制所设置的大小,但还没有超过硬性限制,那么服务器将使用服务器状态结构的 obuf_soft_limit_reached_time 属性记录下客户端

到达软性限制的起始时间,之后服务器会继续监视客户端,如果输出缓冲区的大小一直超出软性限制,并且持续时间超过服务器设定的时长,那么服务器就会关闭客户端,相反地,如果客户端在指定时间内不再超出软性限制,那么客户端就不会被关闭,并且obuf_soft_limit_reached_time也会被清零。

2.3、Lua脚本的伪客户端

服务器会在初始化时创建负责执行Lua脚本中包含的Redis命令的伪客户端,并将这个伪客户端关联在服务器状态结构的lua_client属性中。lua_client伪客户端在服务器运行的整个生命期中一直存在,只有服务器被关闭时,这个客户端才会被关闭。

2.4、AOF文件的伪客户端

服务器在载入AOF文件时,会创建用于执行AOF文件包含的Redis命令的伪客户端,并在载入完成之后,关闭这个伪客户端。

 

 参考《Redis设计与实现》

 https://www.cnblogs.com/undefined22/p/12580818.html

https://blog.csdn.net/sinat_30333853/article/details/80917044 

posted on 2021-05-09 11:59  duanxz  阅读(891)  评论(0编辑  收藏  举报