文章目录
Redis是一个事件驱动程序,服务器需要处理两类事件:
1.文件事件
2.时间时间
一、文件事件
Redis通过套接字和客户端连接, 文件事件就是服务器对套接字操作的抽象。
其基于Reactor模式开发了自己的网络事件处理器,该处理器被称为文件事件处理器。
-
基于IO复用程序可同时监听多个套接字,根据套接字执行的任务为套接字关联不同的事件处理器
-
被监听的套接字准备好执行连接应答(accept)、读取、写入和关闭等操作时,就会产生和它对应的文件事件。
而文件事件处理器就会调用关联好的事件处理器处理事件。
1.0 Reactor模式
大概就是,具体事件处理程序会向反应器注册一个事件处理器,表示自己关注这些事件,当该事件存在的时候,具体事件处理程序就会通过注册的事件处理器对指定的事件进行处理。
以原生JDK的NIO的单线程Reactor模式为例。
其流程如下:
-
服务端的Reactor为一个线程对象,会启动事件循环,使用多路复用器(Selector)实现IO多路复用。
-
注册一个Acceptor事件到Reactor中,该事件关注accept事件,当Reactor监听到客户端向服务端发起的连接请求时,就会将Accept事件派发给响应的accept处理器处理。
然后将该连接关注的read事件和对应的read事件处理器注册到Reactor中,Reactor就会监听连接的read事件
-
Reactor监听到read或write事件时,相关的事件交给响应 的处理器处理。
-
每处理完所有就绪的关注的IO事件后,Reactor线程会再次执行select()阻塞监听新的事件就绪,然后交给事件处理器处理。
Redis的文件事件处理器也是以单线程运行,但通过IO多路复用程序监听多个套接字,可以实现高性能的网络通信模型,单线程运行的方式也可以和其他单线程方式运行的模块对接。
1.1 文件事件处理器的构成
- 每当一个套接字准备好执行连接应答、写入、读取和关闭等操作时,就会产生一个文件事件;
- IO多路复用程序监听多个套接字,然后将那些产生文件事件的套接字放到一个队列中,按序、同步每次一个套接字的方式将套接字传给文件事件分派器;
- 文件事件分派器会将事件分派给对应的处理器进行处理;
- 处理完毕后,IO多路复用程序才会将队列中的下一个分给分派器
1.2 事件类型
IO多路复用程序可监听多个套接字的AE_READABLE
事件和AE_WRITABLE
事件:
- 套接字变可读时(客户端对套接字进行write或close操作),或有新的应答套接字出现时(客户端对服务端的监听套接字执行connect操作),套接字会产生
AE_READABLE
事件; - 套接字可写时(客户端对套接字执行read操作),套接字会产生
AE_WRITABLE
事件
如果同时出现这两个事件,则先处理AE_READABLE
事件,处理完事后,再处理AE_WRITABLE
事件。
1.3 文件事件处理器
-
连接应答处理器
与监听套接字关联,用于对连接服务器的多个客户端进行应答
-
命令请求处理器
为客户端套接字关联此处理器,用于接收客户端的命令请求
-
命令回复处理器
为客户端套接字关联此处理器,用于向客户端返回命令执行的结果
-
复制处理器
用于主服务器向从服务器进行复制操作
1.3.1连接应答处理器
对应acceptTcpHandler
函数。
服务器初始化时,将连接应答处理器和服务器监听套接字的AE_READABLE
事件关联,这样客户端调用connect
连接时,套接字会产生AE_READABLE
事件,触发连接应答处理器处理。
1.3.2命令请求处理器
对应readQueryFromClient
函数。
客户端通过连接应答处理器连接成功后,服务器会将客户端的AE_READABLE
事件和命令请求处理器关联,让客户端可以发送命令。
这样客户端发送命令请求时,会产生AE_READABLE
事件,触发命令请求处理器进行处理。
1.3.3 命令回复处理器
对应sendReplyToClient
函数。
服务器执行完命令后,需要将执行结果返回给客户端,这时就会将客户端的AE_WRITABLE
事件和命令回复处理器关联。
客户端准备好接收服务器的回复后,会产生AE_WRITABLE
事件,然后出发命令回复处理器执行。
执行完毕后,会解除命令回复处理器和客户端AE_WRITABLE
事件的关联。
二、时间事件
Redis时间事件有两类:
-
定时事件
让某程序在指定时间后执行一次。
对应的事件处理器会返回
AE_NOMORE
,该事件到达一次后就删除。 -
周期事件
每隔指定的时间执行一次。
对应的事件处理器返回非
AE_NOMORE
的整数,然后根据该整数更新其when的属性,可以一个周期后再次到达进行处理。【目前只使用了周期事件】
时间事件的属性
-
id
唯一id,从小到大递增,新事件的ID号大
-
when
时间戳,单位ms,时间事件到达的时间。
-
timeProc
时间事件处理器(函数),时间事件到达时,会调用对应的时间事件处理器进行处理
实现
服务器将所有的时间事件放在一个无序链表中(不按when属性大小排序),每当时间事件执行器运行时,就会遍历整个链表查找所有已到达的时间事件,并调用对应的事件处理器,这样才能保证所有已到达的时间事件都能被处理。
新的事件插入到表头。
时间事件处理器原理
时间事件处理器,就是在一个processTimeEvents
函数中实现,其逻辑大概如下:
- 遍历所有的时间事件(无需链表)
- 判断当前事件是否到达
- 如果事件已到达,则执行对应的事件处理器,得到返回值
- 如果是
AE_NOMORE
,说明是定时事件,将该事件删除; - 否则是周期事件,则按照返回值更新该事件的when属性,让其在指定时间后再次到达
serverCron函数
在前面将清除过期键的时候提到过该函数,该函数就是一个时间事件的应用,它的任务如下:
1. 更新服务器的各类统计信息,如:时间、内存占用、数据库占用等;
2. 清理数据库中的过期键值对
3. 关闭和清理连接失效的客户端
4. 尝试进行AOF或RDB持久化
5. 如果服务器是主服务器,则对从服务器进行定期同步
6. 如果是集群模式,则对集群进行定期同步和连接测试。
serverCron函数的执行周期可以在配置文件中修改:
hz 10
默认每秒执行10次,也就是说,默认每100ms执行一次。
但说明中指示:
范围可在1到500之间,但是通常不建议超过100。应该尽量使用默认值10,并且仅在要求非常低延迟的环境中才将其提高到100。
事件的调度和执行
因为同时存在文件事件和时间事件,所以要对两个事件进行调度。调度任务主要在aeProcessEvents
中执行。
其流程如下:
- 获取到达时间离当前时间最近的时间事件
- 计算距离其到达还有多少ms:remaind_ms
- 如果事件已到达(remaind_ms可能为负数),则重置
remaind_ms
为0 - 根据remaind_ms创建
timeval
结构 - 调用
aeApiPoll(timeval)
等待文件事件,阻塞等待时间由timeval
决定,但如果remaind_ms
为0,该方法会立即返回; - 处理所有产生的文件事件
- 处理所有产生的时间事件
该方法在初始化服务器后,通过一个循环不断循环调用,在服务器关闭前结束调用。
aeApiPoll(timeval)
在指定时间内阻塞等待被设置为监听状态的套接字产生文件事件,有至少一个文件事件产生或超时的时候返回。
因此,可以得出以下结论:
-
aeApiPoll(timeval)
阻塞时长由remaind_ms
决定,避免频繁轮询出现的忙等,确保不会阻塞太久; -
服务器先处理文件事件,再处理时间事件,因此时间事件的执行通常会比设定的时间稍晚一点;
-
因为文件事件是随机产生的,如果当前没有时间事件到达,则处理产生的文件事件,如果处理完还没有时间事件到达,则就继续等待处理文件事件,直到时间事件到达;
-
对两个事件的处理都是同步、有序和原子的。服务器不会抢占执行事件也不会中途中断处理事件。
但时间事件中一些比较耗时的操作如持久化等,会放到子线程或子进程执行。
参考:《Redis设计与实现》