一、事件
Redis服务器是一个事件驱动程序,服务器需要处理两类事件:
1)文件事件(file event)Redis服务器通过套接字与客户端(或者其它Redis服务器)进行连接,文件事件就是服务器对套接字操作的抽象。服务器与客户端的通信会产生相应的文件事件,服务器通过监听并处理这些事件来完成一系列网络通信操作。
2)时间事件(time event)Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,时间事件就是服务器对这类定时操作的抽象。
1、文件事件
Redis基于Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler):
1)文件事件处理器使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
2)当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联的事件处理器来处理这些事件。
1.1、文件事件处理器的构成
四个组成部分:套接字、I/O多路复用程序、文件事件分派器、事件处理器
尽管多个文件事件可能会并发出现,但I/O多路复用程序总是将所有产生事件的套接字都放到一个队列里面,然后通过这个队列,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后、I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。
1.2、I/O多路复用程序的实现
Redis的I/O多路复用程序的所有功能通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现,每个I/O多路复用函数库在Redis源码中都对应一个单独的文件。
因为Redis为每个I/O多路复用函数库都实现了相同的API,所以I/O多路复用程序的底层实现是可以互换的。
1.3、事件类型
ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件
1)当套接字变得可读时(客户端执行write操作、close操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行connect操作),套接字产生AE_READABLE事件。
2)当套接字变得可写时(客户端执行read操作),套接字产生AE_WRITABLE事件。
I/O多路复用程序允许服务器同时监听套接字的AE_READABLE事件和AE_WRITABLE事件,如果一个套接字同时产生了这两种事件、文件事件分派器优先处理AE_READABLE事件、等到AE_READABLE事件处理完之后,才处理AE_WRITABLE事件。优先处理读事件
1.4、API
ae.c/aeCreateFileEvent 函数接受一个套接字描述符、一个事件类型,一个时间处理器作为参数,将给定套接字的给定事件加入到I/O多路复用程序的监听范围内,并对事件和事件处理器进行关联。
ae.c/aeDeleteFileEvent 函数 接受一个套接字描述符、一个监听事件类型 作为参数,让I/O多路复用程序取消对给定套接字的给定事件的监听,并取消事件和事件处理器之间的关联。
ae.c/aeGetFileEvents 函数 接受一个套接字描述符,返回该套接字正在被监听的事件类型:
1)如果套接字没有被任何事件监听,返回AE_NONE
2)如果套接字的读事件正在被监听,返回AE_READABLE
3)如果套接字的写时间正在被监听,返回AE_WRITABLE
4)如果套接字的读事件和写事件同时被监听,返回AE_READABLE | AE_WRITABLE
ae.c/aeWait 函数 接受一个套接字描述符、一个事件类型、一个毫秒数作为参数,在给定的时间内阻塞并等待套接字的给定类型事件产生,当事件成功产生,或者等待超时之后,函数返回。
ae.c/aeApiPoll 函数 接受一个sys/time.h/struct timeval结构体为参数,并在指定时间内,阻塞并等待所有被aeCreateFileEvent函数设置为监听状态的套接字产生的文件事件,当有至少一个事件产生,或者等待超时后,函数返回。
ae.c/aeProcessEvents 函数 是文件事件分派器,它先调用aeApiPoll 函数来等待事件产生,然后遍历所有已产生的事件,并调用相应的事件处理器来处理这些事件。
ae.c/aeGetApiName 函数 返回I/O多路复用程序底层所使用的I/O多路复用函数库的名称:返回"epoll"表示底层为epoll函数库,返回"select"表示底层为select函数库。
1.5、文件事件处理器
Redis为文件事件编写了多个处理器。
1)连接应答处理器
networking.c/acceptTcpHandler 函数 是Redis的连接应答处理器,这个处理器用于对连接服务器监听套接字的客户端进行应答。
2)命令请求处理器
networking.c/readQueryFromClient 函数 是Redis的命令请求处理器,负责从套接字中读入客户端发送的命令请求内容。
3)命令回复处理器
networking.c/sendReplyToClient 函数 是Redis的命令回复处理器,负责将服务器执行命令后得到的命令回复通过套接字返回客户端。
2、时间事件
Redis时间事件分为两类:定时事件、周期性事件
一个时间事件主要由三个属性组成:
1)id: 服务器为时间事件创建的全局唯一ID(标识号),从小到大顺序递增
2)when: 毫秒精度的UNIX时间戳,记录时间事件的到达时间
3)timeProc: 时间事件处理器,一个函数
一个时间事件的类型取决于时间事件处理器的返回值:
1)返回 ae.h/AE_NOMORE 的整数值,为定时事件
2)返回 非ae.h/AE_NOMORE 的整数值,为周期性事件
2.1、实现
服务器将所有时间事件放在一个无序链表中,每当时间事件执行器运行时,就遍历整个链表,查找所有已到达的时间事件,并调用相应的时间事件处理器。
2.2、API
ae.c/aeCreateTimeEvent 函数 接受一个毫秒值和一个时间事件处理器作为参数,将一个时间事件添加到服务器。
ae.c/aeDeleteFileEvent 函数 接受一个时间事件ID作为参数,从服务器中删除该ID所对应的的时间事件。
ae.c/aeSerachNearestTimer 函数 返回到达时间距离当前时间最近的时间事件。
ae.c/processTimeEvents 函数 是时间事件的执行器,会遍历所有已到达的时间事件,并调用这些事件的处理器。
2.3、时间事件应用实例:serverCron函数
Redis定期操作由redis.c/serverCron函数负责执行,主要工作包括:
1)更新服务器各类统计信息,如时间、内存占用、数据库占用
2)清理数据库中的过期键值对
3)关闭和清理连接失效的客户端
4)尝试进行AOF或RDB持久化操作
5)如果是主服务器,对从服务器进行定期同步
6)如果处于集群模式,对集群进行定期同步和连接测试
3、事件的调度与执行
事件的调度和执行由ae.c/aeProcessEvents函数负责
二、客户端
1、客户端属性
客户端包含的属性分为两类:
1)通用属性,无论客户端执行的是什么工作,都要用到的属性
2)非通用属性,和特定功能相关的属性,如操作数据库需要用到的db属性和dictid属性,执行事务需要的mstate属性,以及实行WATCH命令用到的watch_keys属性
1.1、套接字描述符 fd
fd 属性记录了客户端正在使用的套接字描述符,可以是-1或者大于-1的整数:
1)伪客户端的fd属性值为-1
2)普通客户端的fd属性为大于-1的整数
1.2、名字 name
默认情况下,一个连接到服务器的客户端没有名字,使用CLIENT setname 命令可以为客户端设置一个名字
1.3、标志 flags
flags 属性记录了客户端的角色,以及客户端目前所处的状态,可以是单个标志,也可以是多个标志的二进制 或运算值
flags = <flag>
flags = <flag1> | <flag2>| ...
每个标志使用一个常量表示,一部分标志记录客户端的角色,另一部分记录客户端目前所处状态
1.4、输入缓冲区 querybuf
客户端输入缓冲区用于保存 客户端发送过来的命令请求
1.5、命令参数 argv 与命令参数个数 argc
在服务器将客户端发送的命令请求保存到客户端状态的querybuf属性之后,服务器将对命令请求进行分析,并将得出的命令参数及命令参数个数分别保存到客户端状态的argv属性 和argc属性。
argv是一个数组,每一项都是一个字符串对象,argv[0] 是要执行的命令,之后的其他项是命令参数
argc属性记录argv数组的长度
1.6、命令的实现函数cmd
服务器根据argv[0] 的值,在命令表中查找命令所对应的命令实现函数,命令表是一个字典,键是SDS结构,保存命令的名称,字典的值是命令所对应的redisCommand结构,保存命令的实现函数、命令标志、命令应该给定的参数个数、命令的总执行次数和总消耗时长等信息。
1.7、输出缓冲区
执行命令所得的命令回复会保存在客户端状态的输出缓冲区里,每个客户端有两个输出缓存区可用,一个大小固定,另一个大小可变。
1)固定大小的缓冲区用于保存长度较小的回复,如OK、简短的字符串值、整数值、错误回复等
buf是一个大小为REDIS_REPLY_CHUNK_BYTES字节的字节数组,bufpos记录buf数组目前已使用的字节数量。
REDIS_REPLY_CHUNK_BYTES 默认值为16*1024 , buf数组默认大小为16KB。
2)可变大小的缓冲区用于保存长度比较大的回复。
当buf数组空间用完,或者回复太大而没有办法放进buf数组内,服务器会使用可变大小缓冲区。
可变大小缓冲区有reply链表和一个或多个字符串对象组成。
1.8、身份验证 authenticated
authenticated 值为0 ,表示客户端未通过身份验证;为1,表示客户端已经通过身份验证。
authenticated 属性仅在服务器启动身份验证功能时使用
1.9、时间
ctime 记录创建客户端的时间
lastinteraction 属性记录客户端与服务器最后进行互动的时间
obuf_soft_limit_reached_time 属性记录了输出缓冲区第一次到达软性限制的时间
2、客户端的创建与关闭
2.1、创建普通客户端
客户端通过网络连接服务器,在客户端使用connect函数连接到服务器时,服务器会调用连接事件处理器,为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构clients链表的末尾。
2.2、关闭普通客户端
1)客户单进程退出或者被杀死,连接关闭,从而造成客户端被关闭
2)客户端向服务器发送带有不符合协议格式的命令请求,客户端也会被服务器关闭
3)客户端成为了 CLIENT KILL命令的目标
4)用户为服务器设置了timeout选项,当客户端空转时间超过timeout选项设置的值时,客户端将被关闭。
5)如果客户端发送的命令请求个数大小超过了输入缓冲区的限制大小(默认1GB)
6) 如果发送给客户端的命令回复大小超过了输出缓冲区的限制大小
2.3、Lua脚本的伪客户端
服务器会在初始化时,创建负责执行Lua脚本中包含的Redis命令的伪客户端,并将其关联到lua_client属性中
lua_client伪客户端在服务器运行的整个生命周期中会一直存在,只有服务器被关闭,这个客户端才会被关闭
2.4、AOF文件的伪客户端
服务器在载入AOF文件时,会创建用于执行AOF文件包含的Redis命令的伪客户端,在载入完成之后,关闭这个客户端。