第 12 章 事件
Redis 服务器是一个事件驱动程序,需要处理以下两类事件:
- 文件事件:服务器通过套接字与客户端相连,文件事件即服务器对套接字操作的抽象;服务器与客户端的通信会产生相应的文件事件,服务器通过监听和处理事件拉完成一系列网络通信操作
- 时间事件:服务器中的一些操作需要在给定的时间点执行,时间事件则是对这类定时操作的抽象
12.1 文件事件
Redis 基于 Reactor 模式开发自己的网络时间处理器:文件事件处理器
-
使用 I / O 多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器
-
当套接字准备好执行连接应答、读取、写入、关闭等操作时,与之对应的文件事件产生,文件处理器调用套接字之前关联的事件处理器来处理
虽然文件处理器是单线程,但通过 I / O 多路复用监听多个套接字,可以实现了高性能的网络通信模型,又能很好与服务器中单线程的模块进行对接,保证了 Redis 内部单线程的简单性
12.1.1 文件事件处理器的构成
服务器通常连接多个套接字,多个文件事件可能并发地出现,但 I / O 复用总是将所有产生事件的套接字放到一个队列中,有序、同步、每次一个套接字向文件事件分派器传送套接字,只有上一个套接字产生的事件处理完毕(即该套接字为事件所关联的事件处理器执行完毕),才会向文件事件分派器传送下一个套接字
12.1.2 I / O 多路复用程序的实现
所有功能都是通过包装常见的 select、epoll、evport、kqueue 等多路复用函数库,在 Redis 中对应 ae_select.c等
Redis 为每个多路复用函数库都实现了相同的 API,I / O 多路复用程序底层实现是可以互换的
12.1.3 事件的类型
I / O 多路复用程序可以监听多个套接字的 ae.h/AE_READABLE 事件和 ae.h/WRITEABLE 事件
- 套接字变可读(客户端对套接字执行 write 操作或 close 操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行 connect 操作)时,产生 AE_READABLE 事件
- 套接字变得可写时(客户端对套接字执行 read 操作),套鸡爪子产生 WRITEABLE 事件
- 可读可写均相对与服务器来说
如果一个套接字同时产生这两种事件,文件分派器会优先处理 AE_READABLE 事件
即如果一个套接字又可读又可写,则服务器先读再写套接字
12.1.4 API
ae.c
-
aeCreateFileEvent:套接字描述符、事件类型、事件处理器作为参数,将给定套接字的给定事件加入到 I / O 多路复用程序的监听范围内,并对事件和事件处理器进行关联
-
aeDeleteFileEvent:套接字描述符、事件类型作为参数,让 I / O 多路复用程序取消对给定套接字的给定事件的监听,并取消事件和事件处理器之间的关联
-
aeGetFileEvents:套接字描述符作为参数,返回该套接字正在被监听的事件类型
- 没被监听:AE_NONE
- 读事件被监听:AE_READABLE
- 写事件被监听:AE_WRITEABLE
- 都被监听:AE_READABLE | AE_WRITEABLE
-
aeWait:套接字描述符、事件类型、毫秒数为参数,在给定的时间内阻塞并等待套接字的给定类型事件的产生,当事件产生成功或者等待超时则返回
-
ae.ApiPoll:sys/time.h/struct timeval 作为参数,在指定时间内,阻塞并等待所有被 aeCreateFileEvent 设置为监听状态的套接字产生的文件事件,当有文件事件产生或者等待超时则返回
-
aeProcessEvents:文件事件分派器,调用 aeApiPoll 等待事件,再遍历已产生事件,调用响应事件处理器处理
-
aeGetApiName:返回 I / O 多路复用程序底层所使用的 I / O 多路复用函数库的名称
12.1.5 文件事件的处理器
- 连接应答处理器:对连接服务器的各个客户端进行应答
- 请求处理器:接收客户端传来的命令请求
- 回复处理器:向客户返回命令的执行结果
- 复制处理器:当主服务器和从服务器进行复制操作
1.连接应答处理器
networking.c/acceptTcpHandler 函数是 Redis 的连接应答处理器,为 sys/socket.h/accept 函数的包装
初始化时,此处理器会和服务器监听套接字的 AE_READABLE 事件关联,当有客户端用 sys/socket.h/connect 函数连接服务器监听套接字时,套接字产生 AE_READBALE 事件,引发连接应答处理器执行
2.命令请求处理器
networking.c/readQueryFromClient 是命令请求处理器,负责从套接字中读入客户端发送的命令请求内容,为 unistd.h/read 函数的包装
当客户端已经通过连接应答处理器连接到服务器之后,此处理器会和服务器监听套接字的 AE_READABLE 事件关联,当客户端发送命令请求后,套接字产生 AE_READBALE 事件,引发命令请求处理器执行
3.命令回复处理器
networking.c/sendREplyToClient 为命令回复处理器,为 unistd.h/write 的包装
当服务器有命令回复给客户端时,AE_WRITEABLE 事件和命令回复处理器关联
命令回复完毕之后会解除关联
4.一次完整的客户端与服务器连接事件示例
假设 Redis 服务器在运行,监听套接字的 AE_READABLE 事件在监听下,对应处理器为连接应答处理
如果一个 Redis 客户端向服务器发起连接,监听套接字会产生 AE_READABLE 事件,触发连接应答处理器执行,处理器会对请求进行应答,然后创建客户端套接字,以及客户端状态,并将 AE_READABLE 事件与命令处理器关联,使得客户端可以向主服务器发送命令请求
客户端向服务器发送命令请求,客户端则产生 AE_READABLE 事件,引发命令请求处理器执行,处理器读取了客户端的命令内容,传给相关程序去运行
执行命令产生了命令回复,服务器将客户端套接字的 AE_WRITEABLE 事件与命令回复处理器关联,客户端进行读取产生此事件,命令回复处理器将内容写进套接字,服务器最后解除客户套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联
12.2 时间事件
Redis 时间事件分为:
- 定时事件:指定时间运行一次
- 周期事件:每隔一段时间执行一次
时间事件三个属性组成:
- id:时间事件全局唯一 ID,递增
- when:毫秒精度的 UNIX 时间戳,时间事件的到达时间
- timeProc:时间处理器,函数,处理事件
时间事件的返回值决定时间事件的分类:
- ae.h/AE_NOMORE:定时事件,在到达一次之后就会被删除,不再到达
- 非 AE_NOMORE 的整数值:周期性事件,时间事件到达之后,会根据返回值更新 when 属性,一直循环下去
Redis 3.0 只使用 周期性事件,无定时事件
12.2.1 实现
所有时间事件存放在无序链表中,时间事件执行器遍历整个链表,查找已经到达的时间事件,调用相应的事件处理器
无序指when属性无效,链表实际按 ID 排序
3.0 版本,正常模式下的 Redis 服务器只使用 serverCron 一个时间事件,在 benchmark 模式下,也只使用两个时间事件,这种情况下无序链表的长度很短,不会影响事件执行性能
12.2.2 API
ae.c
-
aeCreateTimeEvent:毫秒数,时间事件处理器为参数,将新的时间事件添加到服务器
-
aeDeleteFileEvent:时间事件 ID ,删除对应的时间事件
-
aeSearchNearestTimer:返回到达时间距离当前时间最接近的时间事件
-
processTimeEvents:时间事件的执行器,遍历所有已到达的时间事件,调用事件的处理器
-
已到达:when 属性的时间戳小于等于当前时间的时间戳
-
伪代码:
-
12.2.3 时间事件应用实例:ServerCron 函数
redis.c/serverCron :定期对服务器自身资源和状态进行检查和调整,确保服务器长期、稳定地运行
-
更新服务器的各类统计信息:时间、内存占用、数据库占用等
-
清理数据库中的过期键值对
-
关闭和清理失效的客户端
-
尝试进行 AOF 或者 RDB 持久化操作
-
主服务器需要对从服务器进行定期同步
-
集群模式:对集群进行定期同步和连接测试
周期性事件运行 serverCron,hz 选项调整 serverCron 每秒执行次数
12.3 事件的调度与执行
ae.c/aeProcessEvents 函数复杂调度文件事件和时间事件
将 aeProcessEvents 放在一个主循环中,再加上初始化和清理函数,构成了简化的 Redis 服务器的主函数
事件调度和执行规则:
-
aeApiPoll 最大阻塞时间由已到达的时间最接近当前时间的时间事件决定,可以避免服务器对时间事件频繁的轮询,也可确保函数不会阻塞过长时间
-
文件事件是随机的,如果等待并处理完文件事件后没有时间事件到达,则再次等待并处理文件事件,不断执行直到时间事件到到达时间,则处理时间事件
-
对两种事件的处理都是同步、有序、原子地,不会中断、其他事件抢占,尽可能地减少阻塞时间,并在有需要让出执行权,避免饥饿;如命令回复写入客户端套接字数据超过预设量则使用 break 跳出循环,等待下次;时间事件则将耗时的持久化操作放在子进程、子线程执行
-
因为时间事件在文件事件之后执行,且事件之间不抢占,所以实际 的时间事件处理时间是稍晚的