Redis线程IO模型

1,962 阅读2分钟

Redis是单线程模型,为什么这么快,主要原因是数据全都在内存中。那么单线程又是如何做到可以并发处理那么多客户端连接?这就要归结于它的IO模型:非阻塞IO

阻塞IO

当使用socket套接字时,模式是阻塞IO,比如read方法要传入需要读取多少字节,读全了再返回,如果没有读到那么多个字节,线程就会卡在那儿。write方法一般不会阻塞,只有在套接字的写缓冲区写满了以后,才会阻塞。


上面这张图可以看出在操作系统内核,wait for data 和copy data from kernel to user都是阻塞的,对于redis的单线程模型,只要有一个客户端连接没有读取到数据,整个线程都会卡死在wait for data阶段,这个显然不合理。

非阻塞IO

非阻塞IO,读写方法不会阻塞,能读多少读多少,能写多少写多少。非阻塞IO有个问题,系统如何知道何时可以去读取内容(第一次读取只读到了一部分,还有一部分没读),何时可以去写没有写完的内容(第一次写写满了,还剩下一些)?这就需要知道多路复用(事件轮询)的机制。

最简单的事件轮询API是select,他是操作系统提供给用户程序使用的API。输入是读写文件描述符列表 read_fds & write_fds,输出是与之对应的读写事件。同时还提供timeout参数,防止事件一直处于等待状态。这个select对应的逻辑大致是:

while() {    
    read_events, write_events = select(read_fds, write_fds, timeout)
    for event in read_events:
        handle_read(event.fd)    
    for event in write_events:
        handle_write(event.fd)    
    handle_others() # 处理其它事情,如定时任务等 
 }					
}	

在linux操作系统中,select有个问题是当文件描述符列表过大(一个client连接会对应一个文件描述符),性能会很差,一般使用epoll代替


上图可以看出,wait for data阶段,只要有一个文件描述符有ready的数据,就可以进行处理。对于redis单线程的模型,不会出现任意一个client没有数据ready就会block所有client的数据读写。

指令队列&响应队列

Redis会将每个客户端的套接字都关联一个指令队列,客户端的按指令队列的顺序排队处理,先到先服务

Redis同样也会给每个客户端分配响应队列,redis从响应队列中取出数据写到套接字,作为服务端对客户端的返回。这里有一个优化:如果响应队列为空,说明没有数据需要写入到套接字,也就是不需要去获取套接字的写事件,所以redis会把对应的客户端的文件描述符从write_dfs移除,防止select的时候,马上返回写事件造成CPU飙高。