面对高吞吐量的访问需求,同一个db这个key-value的hashtable面临着来自客户端的并发访问。这个过程中hashtable可能会访问相同桶、rehase,如何保证客户端并发访问时hashtable的线程安全?Redis的做法很直接:单线程的处理来自所有客户端的并发请求。
多路复用
Redis服务器端对于命令的处理是单线程的,但是I/O层面却同时面向多个客户端并发的提供服务,并发到内部单线程的转化通过多路复用框架实现,如下图所示。
上图中粗线描述的就是这个单线程主循环的主要逻辑:
- 首先从多路复用框架中select出已经ready的fileDescriptor,aeApiPoll函数的实现根据实际宿主机器的具体环境分为4种实现方式:epoll、evport、kqueue,以上实现都找不到时使用select这种最通用的方式。
- ready的标准是依据每个fd的interestSet,如已有数据到达kernel(AE_READABLE)、已准备好写入数据(AE_READABLE)。
- 对于上一步已经ready的fd,redis会分别对每个fd上已ready的事件进行处理,处理完相同fd上的所有事件后,再处理一下ready的fd。fd的事件处理逻辑根据所属场景主要分为3种实现:
acceptTcpHandler处理redis的serverSocket上来自客户端的连接建立请求。他会为客户端对应的id注册其关注的事件(interestSet):AE_READABLE,以便感知该fd后续发来的数据;
readQueryFromClient处理来自客户端的数据,他会读取每一个完整的命令并执行,再将执行结果暂存,待客户端对应fd准备好写时向客户端写入。所以该方法需要为fd注册AE_WRITABLE事件并以sendReplyToClient作为处理器。所以该方法需要为fd注册AE_WRITABLE事件并以sendReplyToClient作为处理器。对于multi(批处理的事务),需等到multi包含一个全部的命令时才进行执行;
sendReplyToClient将暂存的执行结果写回客户端。
- 对来自客户端的命令执行结束后,接下来处理定时任务(processEvent)。
- asApiPoll的等待时间取决于定时任务处理(TimeEvents)逻辑。
- 本次主循环完毕,进入下一次主循环的beforeSleep逻辑,后者负责处理数据过期、增量持久化的文件写入等任务。
定时任务处理
在主线程的主循环执行过程中,有一个对象持续的流转并记录着Redis的事件状态,Redis就是由这些事件驱动运转的:
typedef struct aeEventLoop {
int maxfd;
int setsize;
long long timeEventNextId;
time_t lastTime;
aeFileEvent *events;
aeFiredEvent *fired;
aeTimeEvent *timeEventHead;
int stop;
void *apidata;
aeBeforeSleepProc *beforesleep;
} aeEventLoop;
其中events和fired共同维护着和多路复用框架交互的各个事件,而aeTimeEvent*以链表的形式维护着待处理的定时任务(aeTimeEvent):
typedef strut aeTimeEvent{
long long id;
long when_sec;
long when_ms;
aeTimeProc *timeProc;
aeEventFinalizerProc *finalizerProc;
void *clientData;
struct aeTimeEvent *next;
} aeTimeEvent;
其中两个when_表示这个任务下一次执行时间,timeProc表示定时任务的执行逻辑。timeProc函数的返回值为这个任务执行完了以后下次再执行的时间间隔,对于周期性的定时任务,timeProc每次总是返回一个正数(执行间隔),而对于一次性的任务timeProc则返回AE_NOMORE表示“不存在下次执行”。
上节提到“aeApiPoll的等待超时时间取决于定时任务”,redis只有一个线程,循环的处理来自客户端的请求和定时任务,如何保证每个定时任务按时执行呢?下图展示了定时任务的执行过程。
如上图所示,aeApiPoll方法最多等到待处理定时任务中最近的一个的执行时刻,如果多路复用框架未返回已ready的事件,则Redis直接执行定时任务。
每次一个aeTimeEvent执行结束后,都会根据其timeProc方法的返回值更改其下次执行的时间(when_sec/when_ms),如果timeProc返回AE_NOMORE,则将aeTimeEvent删除之。
默认情况下,Redis指挥有一个周期性定时任务serverCron存在,他负责:
- 主动的处理过期key
- 执行命令执行频度、网络读写、内存使用等统计信息
- hash表的增量rehase
- 内存数据持久化逻辑(GBSAVE/AOF)
- 清理过期的客户端连接
- 主备复制的重连
serverCron通过redisServer类的hz属性进行控制。