文章目录
  1. 1. redis为什么设计为单线程
  2. 2. Redis单线程为什么还能这么快?
  3. 3. 非阻塞IO
  4. 4. 事件轮询 (多路复用)
  5. 5. 指令队列
  6. 6. 响应队列
  7. 7. 定时任务
  8. 8. 总结

Redis是单进程单线程模型的KV数据库,那为什么还常应用在高并发场景中? 其中一个重要原因是Redis是一个单进程单线程且采用多路I/O复用模型,非阻塞IO技术, 使之可以同时处理多个连接请求(减少网络IO耗时), 也不需要关心锁,线程切换等资源消耗问题;

redis为什么设计为单线程

官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦)。

正因为Redis是单线程,所以要小心使用那些时间复杂度为O(n)级别的Redis指令,一不小心就可能会导致Redis卡顿。

但单线程的方式无法发挥多核CPU 性能,不过可通过在单机开多个Redis 实例来完善;

多线程处理可能涉及到锁, 多线程处理会涉及到线程切换而消耗CPU

多进程单线程模型: Nginx(单进程启动只有一个进程, 多进程启动时会有一个Master,多个worker进程), Node.js

单进程多线程模型: MySQL, Memcached

进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)

一个进程中至少有一个线程。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

Redis单线程为什么还能这么快?
  • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

  • 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

  • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

  • 使用多路I/O复用模型,非阻塞IO;

  • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

Redis单线程如何处理那么多的并发客户端连接?
因为Redis采用了多路IO复用非阻塞IO技术, 多路IO复用模型是利用select、poll、epoll可以同时监察多个流的IO事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。

多路指的是多个网络连接,复用指的是复用同一个线程。
采用多路IO复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

非阻塞IO

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

非阻塞IO 在套接字对象上提供了一个选项Non_Blocking,当这个选项打开时,读写方法不会阻塞,而是能读多少读多少,能写多少写多少。能读多少取决于内核为套接字分配的读缓冲区内部的数据字节数,能写多少取决于内核为套接字分配的写缓冲区的空闲空间字节数。读方法和写方法都会通过返回值来告知程序实际读写了多少字节。

有了非阻塞IO意味着线程在读写IO时可以不必再阻塞了,读写可以瞬间完成然后线程可以继续干别的事了。

事件轮询 (多路复用)

非阻塞IO有个问题,那就是线程要读数据,结果读了一部分就返回了,线程如何知道何时才应该继续读。也就是当数据到来时,线程如何得到通知。写也是一样,如果缓冲区满了,写不完,剩下的数据何时才应该继续写,线程也应该得到通知。

事件轮询API 就是用来解决这个问题的,最简单的事件轮询APIselect函数,它是操作系统提供给用户程序的API。输入是读写描述符列表read_fds & write_fds,输出是与之对应的可读可写事件。同时还提供了一个timeout参数,如果没有任何事件到来,那么就最多等待timeout时间,线程处于阻塞状态。一旦期间有任何事件到来,就可以立即返回。时间过了之后还是没有任何事件到来,也会立即返回。拿到事件后,线程就可以继续挨个处理相应的事件。处理完了继续过来轮询。于是线程就进入了一个死循环,我们把这个死循环称为事件循环,一个循环为一个周期。

每个客户端套接字socket都有对应的读写文件描述符。

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() # 处理其它事情,如定时任务等

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

服务器套接字serversocket对象的读操作是指调用accept接受客户端新连接。何时有新连接到来,也是通过select系统调用的读事件来得到通知的。

指令队列

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

响应队列

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

定时任务

服务器处理要响应IO事件外,还要处理其它事情。比如定时任务就是非常重要的一件事。如果线程阻塞在select系统调用上,定时任务将无法得到准时调度。那Redis是如何解决这个问题的呢?

Redis 的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参数。因为 Redis 知道未来timeout时间内,没有其它定时任务需要处理,所以可以安心睡眠timeout的时间。

NginxNode的事件处理原理和Redis也是类似的

总结
  • 引用官方说明,阐述了为何Redis被设计为单线程模型;
  • 进一步阐述了为何单线程模型设计的Redis可以非常快的处理高并发等场景的原因;
  • 对多路复用IO模型及非阻塞IO技术进行了原理阐述及分析;
  • 此外注意到截止目前最新redis文档版本已更新到redis5.0,并引入新的数据类型stream,并对HyperLogLog等作出了很多优化改进;

作者署名:朴实的一线攻城狮
本文标题:redis专题11 线程IO模型
本文出处:http://researchlab.github.io/2018/10/08/redis-11-redisio/
版权声明:本文由Lee Hong创作和发表,采用署名(BY)-非商业性使用(NC)-相同方式共享(SA)国际许可协议进行许可,转载请注明作者及出处, 否则保留追究法律责任的权利。

@一线攻城狮

关注微信公众号 @一线攻城狮

总访问:
总访客: