简介
众所周知,Redis是单线程模型,单线程模型为什么Redis处理能力这么快?
非阻塞IO
当调用套接字读写方法,默认是阻塞的,比如 read
方法传进去一个 n
,表示获取 n
个字节后再返回,如果没有字节,线程就会卡在这里,知道数据到来或者链接关闭,read
方法才可以返回。
非阻塞IO提供了一个 Non_Blocking
的选项,当打开时,不会阻塞读写。
- 能读多少,取决于内核为套接字分配读缓冲区内部数据的字节数
- 能写多少,取决于内核为套接字分配写缓冲区空闲空间的字节数
- 读写能通过程序返回值老告知实际读写多少字节
事件轮询 (多路复用)
非阻塞IO 有个问题,读数据只读一部分就返回了,程序如果知道继续读。或者,写的缓冲区满了,接来下的怎么继续写。
- 事件轮询API解决了上述问题,最简单的事件轮询 API 是
select
函数,它是操作系统提供给用户程序的 API。 - 输入是读写描述符列表
read_fds & write_fds
,输出是与之对应的可读可写事件。 - 拿到事件后,线程就可以继续挨个处理相应的事件。处理完了继续过来轮询。于是线程就进入了一个死循环,我们把这个死循环称为事件循环,一个循环为一个周期。
- 同时还提供了一个
timeout
参数,如果没有任何事件到来,那么就最多等待timeout
时间,线程处于阻塞状态。 - 我们通过
select
系统调用同时处理多个通道描述符的读写事件,因此我们将这类系统调用称为多路复用 API。
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() # 处理其它事情,如定时任务等
复制代码
现代操作系统的多路复用 API 已经不再使用select
系统调用,而改用 epoll(linux)
和 kqueue(freebsd & macosx)
,因为 select 系统调用的性能在描述符特别多时性能会非常差。它们使用起来可能在形式上略有差异,但是本质上都是差不多的。
指令队列
Redis 会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行顺序处理,先到先服务。
响应队列
Redis 同样也会为每个客户端套接字关联一个响应队列。Redis 服务器通过响应队列来将指令的返回结果回复给客户端。 如果队列为空,那么意味着连接暂时处于空闲状态,不需要去获取写事件,也就是可以将当前的客户端描述符从write_fds
里面移出来。等到队列有数据了,再将描述符放进去。避免select
系统调用立即返回写事件,结果发现没什么数据可以写。出这种情况的线程会飙高 CPU。
定时任务
如果线程阻塞在 select 系统调用上,定时任务将无法得到准时调度。那 Redis 是如何解决这个问题的呢?
Redis 的定时任务会记录在一个称为最小堆
的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select
系统调用的timeout
参数。因为 Redis 知道未来timeout
时间内,没有其它定时任务需要处理,所以可以安心睡眠timeout
的时间。
总结
Redis为什么这么快
- 数据是存在内存,所有的计算都是内存级别
- 数据结构简单,很多操作时间复杂度都是O(1)
- 单线程设计,避免了上下文锁的竞争,避免多线程切换对CPU的影响
- 使用非阻塞IO,多路I/O复用模型
备注
以上内容摘自 《Redis 深度历险:核心原理与应用实践》钱文品