欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

redis 客户端实现

程序员文章站 2022-05-28 12:09:53
...
        Redis 服务器是典型的一对多服务器程序,通过使用由 I/O 多路复用技术实现的文件事件处理器,Redis 服务器使用单进程单线程的方式来处理命令请求,并与多个客户端进行网络通信。Redis 服务器状态结构 redisServer 的 clients 链表属性就保存了所有连接的客户端的状态结构,对客户端执行批量操作或者查找某个指定的客户端,都可以通过遍历 clients 链表来完成。
        对于每个连接的客户端,服务器都为之建立了相应的 redisClient 结构,其中保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构,包括:
        * 客户端的套接字描述符。
        * 客户端的名字。
        * 客户端的标志值。
        * 指向客户端正在使用的数据库的指针,以及该数据库的号码。
        * 客户端当前要执行的命令、命令的参数、参数的个数,以及指向命令实现函数的指针。
        * 客户端的输入缓冲区和输出缓冲区。
        * 客户端的复制状态信息,以及进行复制所需的数据结构。
        * 客户端执行 BRPOP、BLPOP 等列表阻塞命令时使用的数据结构。
        * 客户端的事务状态,以及执行 WATCH 命令时使用的数据结构。
        * 客户端执行发布与订阅功能时用到的数据结构。
        * 客户端的身份验证标志。
        * 客户端的创建时间、和服务器最后一次通信的时间,以及输出缓冲区大小超出软性限制的时间。
        接下来就将对客户端状态中比较通用的那部分属性进行介绍,并讲述服务器创建并关闭各种不同类型的客户端的方法,至于那些和特定功能相关的属性则留待后面介绍相应功能时再行介绍。

        一、客户端的套接字描述符和名字
        客户端状态结构 redisClient 的 fd 属性记录了客户端正在使用的套接字描述符,根据客户端类型的不同,fd 属性的值可以是 -1 或者是大于 -1 的整数:
        1)伪客户端(fake client)的 fd 属性的值为 -1:目前 Redis 服务器会在两个地方用到伪客户端,一个是用于载入 AOF 文件并还原数据库状态,另一个是用于执行 Lua 脚本中包含的 Redis 命令。这种客户端不需要用到网络,自然也不需要记录套接字描述符。
        2)普通客户端的 fd 属性的值为大于 -1 的整数。
        客户端的名字记录在 redisClient 结构的 name 属性里面。默认情况下,一个连接到服务器的客户端是没有名字的,使用“CLIENT SETNAME”命令可以为客户端设置一个名字,让其身份变得更清晰。
        Redis 命令“CLIENT LIST”的输出中,fd 字段和 name 字段就对应该客户端的套接字描述符和名字。

        二、标志
        redisClient 结构的 flags 标志属性记录了客户端的角色,以及客户端目前所处的状态。flags 属性的值可以是单个标志,也可以是多个标志的二进制或,每个标志使用一个常量表示,一部分标志记录了客户端的角色:
        * REDIS_MASTER 和 REDIS_SLAVE:在主从服务器进行复制操作时,主服务器会成为从服务器的客户端,而从服务器也会成为主服务器的客户端。REDIS_MASTER 标志表示客户端代表的是一个主服务器,REDIS_SLAVE 标志则表示客户端代表的是一个从服务器。
        * REDIS_PRE_PSYNC:表示客户端代表的是一个版本低于 Redis2.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_MULTI:表示客户端正在执行事务。
        * REDIS_DIRTY_CAS 和 REDIS_DIRTY_EXEC:前者表示事务使用 WATCH 命令监视的数据库键已经被修改,后者表示事务在命令入队时出现了错误。这两个标志只能在打开了 REDIS_MULTI 标志的情况下使用,它们都表示事务的安全性已被破坏,只要其中任意一个被打开,EXEC 命令必然会执行失败。
        * REDIS_CLOSE_ASAP:表示客户端的输出缓冲区大小超出了服务器允许的范围,服务器会在下一次执行 serverCron 函数时关闭这个客户端,以免服务器的稳定性受到这个客户端影响。积存在输出缓冲区中的所有内容会直接被释放,不会返回给客户端。
        * REDIS_CLOSE_AFTER_REPLY:表示有用户对这个客户端执行了“CLIENT KILL”命令,或者客户端发送给服务器的命令请求中包含了错误的协议内容。服务器会将客户端积存在输出缓冲区中的所有内容发送给客户端,然后关闭客户端。
        * REDIS_ASKING:表示客户端向集群节点(运行在集群模式下的服务器)发送了 ASKING 命令。
        * REDIS_FORCE_AOF 和 REDIS_FORCE_REPL:前者强制服务器将当前执行的命令写入到 AOF 文件,后者强制主服务器将当前执行的命令复制给所有从服务器。执行 PUBSUB 命令会使客户端打开 REDIS_FORCE_AOF 标志(因为 PUBSUB 命令会使所有接收到订阅消息的客户端的状态发生改变),执行“SCRIPT LOAD”命令则会使客户端同时打开这两个标志(因为这个命令会修改服务器的状态)。
        * REDIS_MASTER_FORCE_REPLY:在主从服务器进行命令传播期间,从服务器需要向主服务器发送“REPLICATION ACK”命令,在发送该命令之前,从服务器必须打开主服务器对应的客户端的这个标志,否则发送操作会被拒绝执行。

        三、输入缓冲区、命令与命令参数及命令的实现函数
        redisClient 结构的 querybuf 属性表示客户端的输入缓冲区,用于保存客户端发送的命令请求序列。比如,在收到“SET key value”的命令请求后,querybuf 属性将是一个包含内容为“*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n”的 SDS 值。输入缓冲区的大小会根据输入内容动态地缩小或扩大,但最大不能超过 1GB,否则服务器将关闭这个客户端。
        在服务器将命令请求序列保存到 querybuf 属性后,就将对其内容进行分析,并将得出的命令参数以及参数个数分别保存到 redisClient 结构的 argv 数组属性和 argc 熟悉中。数组元素 argv[0] 就是要执行的命令,之后的项则是传给该命令的参数。argc 属性则负责记录 argv 数组的长度(包括 argv[0])。
        当服务器从协议内容中得出 argv 和 argc 属性的值之后,服务器将根据 argv[0] 的值(不区分大小写)在命令表中查找对应的命令实现函数。该表是一个字典,字典的键是一个 SDS 结构,保存了命令的名字,字典的值是命令所对应的 redisCommand 结构,该结构保存了命令的实现函数、命令的标志、命令应该给定的参数个数、命令的总执行次数和总消耗时长等统计信息。在成功找到 argv[0] 对应的 redisCommand 结构后,服务器就会将 redisClient 结构的 cmd 属性指向这个结构。之后,服务器就可以根据该结构,以及 argv 和 argc 属性中保存的命令参数信息来调用命令实现函数,执行客户端指定的命令。

        四、输出缓冲区
        执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面,每个客户端都有两个输出缓冲区可用:一个缓冲区的大小是固定的,用于保存那些长度比较小的回复;另一个则是可变的,用于保存那些长度比较大的回复。
        固定大小缓冲区由 redisClient 结构的 buf 和 bufpos 两个属性组成,buf 是一个大小为 REDIS_REPLY_CHUNK_BYTES(目前的默认值为 16*1024)字节的字节数组,而 bufpos 属性则记录了 buf 数组目前已使用的字节数量。
        当 buf 数组的空间已经用完,或者因为回复太大时,服务器就会开始使用可变大小缓冲区。可变大小缓冲区由 redisClient 结构的 reply 链表属性来表示。

        五、身份验证和时间
        redisClient 结构的 authenticated 属性用于记录客户端是否通过了身份验证:
        1)当其值为 0 时,表示客户端未通过身份验证,此时除了 AUTH 命令外,客户端发送的所有其他命令都会被服务器拒绝执行。
        2)当其值为 1 时,表示客户端已经通过 AUTH 命令通过了身份验证,这时客户端才可以正常发送命令请求。
        authenticated 属性仅在服务器启用了身份验证功能时使用。如果服务器没有启用这项功能,那么即使 authencicated 为 0,服务器也不会拒绝执行客户端发送的命令请求。关于服务器身份验证的更多信息可以参考配置文件对 requirepass 选项的相关说明。
        最后,客户端 redisClient 状态结构中还有几个和时间有关的属性:
        * ctime:记录了客户端创建的时间,可用之来计算客户端与服务器已经连接了多少秒,“CLIENT LIST”命令输出的 age 字段就记录了这个秒数。
        * lastinteraction:记录了客户端与服务器最后一次进行互动的时间。可用之来计算客户端的空转(idle)时间,“CLIENT LIST”命令输出的 idle 字段就记录了这个秒数。
        * obuf_soft_limit_reached_time:记录了输出缓冲区第一次到达软性限制的时间。

        六、客户端的创建与关闭
        服务器使用不同的方式来创建和关闭不同类型的客户端。
        如果是通过网络来连接服务器的普通客户端,那么在客户端使用 connect 函数连接到服务器时,服务器就会调用连接事件处理器为之创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾。
        一个普通客户端可以因为多种原因而被关闭:
        1)客户端进程退出或者被杀死。
        2)如果客户端向服务器发送了带有不符合协议格式的命令请求,则会被服务器关闭。
        3)客户端成为了“CLIENT KILL”命令的目标。
        4)如果用户为服务器设置了 timeout 配置选项,那么当客户端的空转时间超过该值时,客户端将被关闭。不过 timeout 选项也有一些例外情况:如果客户端是主服务器(打开了 REDIS_MASTER 标志)、从服务器(打开了 REDIS_SLAVE 标志)、正在被 BLPOP 等命令阻塞(打开了 REDIS_BLOCKED 标志),或者正在执行 SUBSCRIBE、PSUBSCRIBE 等订阅命令时,那么即使客户端的空转时长超过了 timeout 的值,客户端也不会被关闭。
        5)客户端发送的命令请求的大小超过了输入缓冲区的限制(默认为 1GB)。
        6)要发送给客户端的命令回复的大小超过了输出缓冲区的限制大小。
        虽然理论上来说,可变大小的输出缓冲区应该可以保存任意长的命令回复,但是,为了避免客户端的回复过大,占用过多的服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作。
        服务器使用两种模式来限制客户端输出缓冲区的大小:
        1)硬性限制(hard limit):如果输出缓冲区的大小超出了该值,则服务器会立即关闭客户端。
        2)软性限制(soft limit):如果输出缓冲区的大小超过了软性限制设置的大小,但还没超过硬性限制,那么服务器将使用客户端状态的 obuf_soft_limit_reached_time 属性记录下客户端到达软性限制的起始时间,之后服务器会继续监视客户端:如果输出缓冲区的大小一直超出软性限制,并且持续时间超过服务器设置的时长,那么服务器将关闭客户端。
        使用 client-output-buffer-limit 选项可以为普通客户端、从服务器客户端,及执行发布与订阅功能的客户端分别设置不同的软性限制和硬性限制。

        七、Lua 脚本和 AOF 文件的伪客户端
        服务器会在初始化时创建负责执行 Lua 脚本中包含的 Redis 命令的伪客户端,并其关联在服务器状态结构 redisServer 的 lua_client 属性中。这个伪客户端在服务器运行的整个生命期中会一直存在,只有服务器被关闭时,它才会被关闭。
        服务器在载入 AOF 文件时,会创建用于执行 AOF 文件包含的 Redis 命令的伪客户端,并在载入完成后,关闭这个伪客户端。


参考书籍:《Redis 设计与实现》第 13 章——客户端。

上一篇: Lua 脚本

下一篇: 数据库复制