众所周知 redis是单线程的,但是他的QPS(Query Per Second) 又很高,也就是很快!那么是为什么呢?
这个许多人都清楚,redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 redis 的 QPS 过万的重要基础。
这个也是重要的原因,了解非阻塞IO之前我们先了解一下什么是阻塞IO。
当我们调用 Scoket 的读写方法,默认它们是阻塞的。
read() 方法要传递进去一个参数 n,表示读取这么多字节后再返回,如果没有读够 n 字节线程就会阻塞,直到新的数据到来或者连接关闭了, read 方法才可以返回,线程才能继续处理。
write() 方法会首先把数据写到系统内核为 Scoket 分配的写缓冲区中,当写缓存区满溢,即写缓存区中的数据还没有写入到磁盘,就有新的数据要写道写缓存区时,write() 方法就会阻塞,直到写缓存区中有空闲空间。
非阻塞 IO 在 Scoket 对象上提供了一个选项Non_Blocking ,当这个选项打开时,读写方法不会阻塞,而是能读多少读多少,能写多少写多少。
能读多少取决于内核为 Scoket 分配的读缓冲区的大小,能写多少取决于内核为 Scoket 分配的写缓冲区的剩余空间大小。读方法和写方法都会通过返回值来告知程序实际读写了多少字节数据。
有了非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞了,读写可以瞬间完成然后线程可以继续干别的事了。
理解了非阻塞 IO才完成一半,我们还要配合着 IO多路复用来理解redis单线程还为什么快。
非阻塞 IO 有个问题,那就是单个线程要处理多个读写请求,处理某个客户端的的读数据的请求,结果读了一部分就返回了,线程如何知道什么时候才应该继续读数据。处理写请求的时候,如果缓冲区满了,写不完,剩下的数据何时才应该继续写?在什么时候处理什么请求?redis 单线程处理多个IO请求时就用到了IO多路复用技术。
IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
如上图,I/O多路复用模型使用了Reactor设计模式实现了这一机制。通过Reactor的方式,可以将用户线程轮询I/O操作状态的工作统一交给handle_events事件循环进行处理。
用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。
当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路I/O复用模型也被称为异步阻塞I/O模型。
注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用I/O多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起I/O请求时,数据已经到达了,用户线程一定不会被阻塞。
可以看到,当有多个IO请求时,redis的线程通过使用 内核的(select 或者 epolls)将多个IO交给socket处理,此时虽然用户的IO请求会等待,但是redis的单线程可以同时处理多个io请求.
这里我们了解了redis是如何用单线程来处理多个io请求的,下面了解redis如何处理多个用户的请求
Redis 会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行,顺序处理,先到先服务。
Redis 同样也会为每个客户端套接字关联一个响应队列。 Redis 服务器通过响应队列来将指令的返回结果回复给客户端。( 如果队列为空,那么意味着连接暂时处于空闲状态,不需要去获取写事件,也就是可以将当前的客户端描述符从 write_fds 里面移出来。等到队列有数据了,再将描述符放进去。避免 select 系统调用立即返回写事件,结果发现没什么数据可以写。出这种情况的线程会飙高 CPU。)(这个没理解等以后理解了再写吧)
每个客户端套接字 将 当前操作 放入到 指令队列,redis的单线程从指令队列中按顺序执行,(通过非阻塞IO + 多路复用)然后将执行结果放到响应队列中,最后再返回给客户执行结果
参考 《Redis 深度历险》 《Redis 单线程模型介绍》 https://cloud.tencent.com/developer/article/1403767