Redis -- 线程IO模型

非阻塞IO

  1. 当调用套接字的读写方法,默认都是阻塞
    • read(n)表示最多读取n个字节后再返回,如果一个字节都没有,那么线程会阻塞,直到有新的数据到来或者连接关闭
    • write一般不会阻塞,除非内核为套接字分配的写缓冲区已经了才会阻塞,直到缓冲区有空闲空间挪出来
  2. 非阻塞IO在套接字对象上提供了一个选项Non_Blocking(读写方法不会阻塞,能读多少读多少,能写多少写多少)
    • 能读多少取决于内核为套接字分配的读缓冲区内部的数据字节数
    • 能写多少取决于内核为套接字分配的写缓冲区的空闲空间字节数
    • 读方法和写方法都会通过返回值告知程序实际读写了多少字节

事件轮询/多路复用

  1. 非阻塞IO存在的问题是没有通知机制,可以通过事件轮询API来解决这个问题
  2. 最简单的事件轮询API是select函数,这是操作系统提供给用户程序的API
    • 输入是读写描述符列表read_fds & write_fds,输出的是与之对应的可读可写事件
    • 同时还提供一个timeout参数,如果没有任何事件到来,那么最多等待timeout时间,线程处于阻塞状态
    • 期间有任何事件到来,就立即返回,时间过了之后,还没有任何事件到来,也会立即返回
    • 拿到事件后,线程可以继续挨个处理相应的事件,事件处理完之后继续过来轮询
    • 这样线程进入一个死循环,称为事件循环
  3. 通过select系统调用同时处理多个通道描述符的读写事件,因此将这类系统调用称为多路复用API
  4. 现代操作系统的多路复用API已经不再使用select系统调用,而是使用epoll(linux)kqueue(freebsd & macosx)
    • 这是因为select系统调用的性能描述符很多时会非常差
  5. 服务端套接字serversocket对象的读操作是指调用accept接受客户端新连接
    • 何时有新连接到来,也是可以通过select系统调用的读事件来得到通知
  6. 事件轮询API就是Java语言层面的NIO
1
2
3
4
5
6
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() # 处理其它事情,如定时任务等

指令队列

  1. Redis会将每个客户端套接字都关联一个指令队列,客户端的指令通过队列来排队进行顺序处理,先到先服务

响应队列

  1. Redis同样会为每个客户端套接字关联一个响应队列
  2. Redis服务器通过响应队列来将指令的返回结果回复给客户端
  3. 如果队列为空,那么意味着连接暂时处于空闲状态,不需要去获取写事件,可以将当前的客户端描述符从write_fds移除
    • 等响应队列里面有值了,再将描述符放进去
    • 避免select系统调用立即返回写事件,结果发现什么数据都没有,这样会导致CPU飙升

定时任务

  1. 服务器处理要响应IO事件外,还需要处理其它事情,例如定时任务
    • 如果线程阻塞在select系统调用上,定时任务将无法得到准确调度
  2. Redis的定时任务会记录在一个最小堆中,在该堆中,最快要执行的任务排在堆的最上方
    • 在每个循环周期,Redis都会将最小堆里面已经到点的任务立即进行处理
    • 处理完毕后,将最快要执行的任务还需要的时间记录下来,该时间就是select系统调用的timeout参数
    • Redis知道未来的timeout时间内,没有其它定时任务要执行,所以可以安心休眠timeout时间
0%