Redis 线程IO

简介

众所周知,Redis是单线程模型,单线程模型为什么Redis处理能力这么快?

非阻塞IO

当调用套接字读写方法,默认是阻塞的,比如 read 方法传进去一个 n,表示获取 n 个字节后再返回,如果没有字节,线程就会卡在这里,知道数据到来或者链接关闭,read 方法才可以返回。

非阻塞IO提供了一个 Non_Blocking 的选项,当打开时,不会阻塞读写。

  • 能读多少,取决于内核为套接字分配读缓冲区内部数据的字节数
  • 能写多少,取决于内核为套接字分配写缓冲区空闲空间的字节数
  • 读写能通过程序返回值老告知实际读写多少字节

事件轮询 (多路复用)

非阻塞IO 有个问题,读数据只读一部分就返回了,程序如果知道继续读。或者,写的缓冲区满了,接来下的怎么继续写。

image.png

  • 事件轮询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 深度历险:核心原理与应用实践》钱文品

猜你喜欢

转载自juejin.im/post/7042942516978516004