理解Redis的线程 IO 模型

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/itsoftchenfei/article/details/84943148

最近一位朋友问到:既然Redis是单线程的工作模式,如何处理那么多的并发客户端连接?

Redis 是个单线程程序!这点必须铭记。也许你会怀疑高并发的 Redis 中间件怎么可能是单线程。很抱歉,它就是单线程,你的怀疑暴露了你基础知识的不足。莫要瞧不起单线程,除了 Redis 之外,Node.js 也是单线程,Nginx 也是单线程,但是它们都是服务器高性能的典范。

优势使用不当就会变成劣势。redis单线程很快,因为它所有的数据都在内存中,所有的运算都是内存级别的运算。正因为 Redis 是单线程,所以要小心使用 Redis 指令,对于那些时间复杂度为 O(n) 级别的指令,一定要谨慎使用,一不小心就可能会导致 Redis 卡顿。

要理解它的高并发,首先要非阻塞 IO多路复用这些词汇

阻塞IO 非阻塞IO
当我们调用套接字的读写方法,默认它们是阻塞的,比如read方法要传递进去一个参数n,表示最多读取这么多字节后再返回,如果一个字节都没有,那么线程就会卡在那里,直到新的数据到来或者连接关闭了,read方法才可以返回,线程才能继续处理。而write方法一般来说不会阻塞,除非内核为套接字分配的写缓冲区已经满了,write方法就会阻塞,直到缓存区中有空闲空间挪出来了。 非阻塞 IO 在套接字对象上提供了一个选项Non_Blocking,当这个选项打开时,读写方法不会阻塞,而是能读多少读多少,能写多少写多少。能读多少取决于内核为套接字分配的读缓冲区内部的数据字节数,能写多少取决于内核为套接字分配的写缓冲区的空闲空间字节数。读方法和写方法都会通过返回值来告知程序实际读写了多少字节。

事件轮询(多路复用)

非阻塞 IO 有个问题,那就是线程要读数据,结果读了一部分就返回了,线程如何知道何时才应该继续读。也就是当数据到来时,线程如何得到通知。写也是一样,如果缓冲区满了,写不完,剩下的数据何时才应该继续写,线程也应该得到通知。事件轮询 API(它是Java 语言里面的 NIO 技术) 就是用来解决这个问题的,最简单的事件轮询 API 是select函数,它是操作系统提供给用户程序的 API。输入是读写描述符列表read_fds & write_fds,输出是与之对应的可读可写事件。同时还提供了一个timeout参数,如果没有任何事件到来,那么就最多等待timeout时间,线程处于阻塞状态。一旦期间有任何事件到来,就可以立即返回。时间过了之后还是没有任何事件到来,也会立即返回。拿到事件后,线程就可以继续挨个处理相应的事件。处理完了继续过来轮询。于是线程就进入了一个死循环,我们把这个死循环称为事件循环,一个循环为一个周期。

因为我们通过select系统调用同时处理多个通道描述符的读写事件,因此我们将这类系统调用称为多路复用 API。现代操作系统的多路复用 API 已经不再使用select系统调用,而改用epoll(linux)kqueue(freebsd & macosx),因为 select 系统调用的性能在描述符特别多时性能会非常差。它们使用起来可能在形式上略有差异,但是本质上都是差不多的,都可以使用上面的伪代码逻辑进行理解。服务器套接字serversocket对象的读操作是指调用accept接受客户端新连接。何时有新连接到来,也是通过select系统调用的读事件来得到通知的。

服务端

redis server就是通过blocking_keys(指令队列)和ready_keys(响应队列)两个数据结构来实现的阻塞操作。但整个阻塞并没有阻塞EventLoop本身,从而实现命令的快速响应。算是一个典型的空间换时间的设计思路。

指令队列

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

响应队列

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

定时任务

服务器处理要响应 IO 事件外,还要处理其它事情。比如定时任务就是非常重要的一件事。如果线程阻塞在 select 系统调用上,定时任务将无法得到准时调度。那 Redis 是如何解决这个问题的呢?Redis 的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参数。因为 Redis 知道未来timeout时间内,没有其它定时任务需要处理,所以可以安心睡眠timeout的时间。

客户端

分两种I/O模型来阐述:阻塞I/O(BIO)和非阻塞I/O(NIO),Jedis是BIO。

BinaryJedis 中一个blpop请求会向redis发起两次I/O请求,一次向redis发送BLPOP key命令,一次从对应的链接管道(channel)中读取数据。由于BIO的特性当channel中没有数据时会一直阻塞,直到有新数据为止。这样就实现了客户端的阻塞效果。
注意:这里的链接是被独享的,不然会有数据干扰。

参考链接:
redis.c源码
t_list.c源码

猜你喜欢

转载自blog.csdn.net/itsoftchenfei/article/details/84943148