原理1:线程IO模型
我们学习Redis的初衷就是因为其高性能,而说到高性能,我们可以会想到多线程、高并发等词汇。但是Redis是一个单线程程序,这点是需要我们铭记的。
那么为什么身为单线程的Redis能够给我们带来如此强大的性能呢?
-
单线程,千万不要先看单线程的应用,单线程对比与多线程少去了切换线程所带来的开销。除了Redis之外,Node.js、Nginx也是单线程,而他们都是服务器高性能的典范
-
内存:Redis是内存型的数据库,对比与MySQL等数据库,其所有的运算都是内存级别的。但正因为Redis是单线程的,所以对于那些时间复杂度为O(n)级别的指令的使用要谨慎,否则会造成Redis卡顿。 tips:Redis中O(n)级别的指令有:KEYS、DEL、LINDEX、HGETALL、SMEMBERS、ZRANGE等这些会遍历KEY的指令
-
多路复用 这是Redis能够处理这么多并发的客户端连接的关键,多路复用设计到两个词汇:NI/O和事件轮询
NIO
当我们调用socket的读写方法时,默认他们都是阻塞的,比如read,方法要传递进去一个参数n,来表示获取n个字节后再返回,如果没有获取到n个字节时就会阻塞;write方法当缓存写满的时候也会阻塞,知道缓存区空出来的时候才继续写入。
这些就属于BIO(Blocking Input/Output),而阻塞I/O对于时间性能的影响很大。而NIO(Non-Blocking Input/Output)非阻塞I/O就是为了让读写方法不再阻塞,能读多少读多少,能写多少写多少。这就意味着读写方法都能够瞬间完成,然后让线程继续干别的事情了。
事件轮询
对于NIO来说有个问题就是线程要读数据,结果读了一部分就返回了,而线程无法知道什么时候才能够读剩余的部分,也就是怎样通知线程来读取剩余的部分。
事件轮询 API 就是用来解决这个问题的,最简单的事件轮询 API 是 select 函数,它是操作系统提供给用户程序的 API。输入是读写描述符列表 read_fds & write_fds,输出是与之对应的可读可写事件。同时还提供了一个 timeout 参数,如果没有任何事件到来,那么就最多等待 timeout 时间,线程处于阻塞状态。一旦期间有任何事件到来,就可以立即返回。时间过了之后还是没有任何事件到来,也会立即返回。拿到事件后,线程就可以继续挨个处理相应的事件。处理完了继续过来轮询。于是线程就进入了一个死循环,我们把这个死循环称为事件循环,一个循环为一个周期。
通过select系统调用同时处理多个通道描述符的读写事件,我们将这类OS调用称为多路复用API,而目前现代操作系统很少用select了,因为其性能较低,取而代之的是epoll(Linux)和kqueque(freebsd & macosx)
指令队列 & 响应队列
对于多个客户端连接,Redis通过指令队列来关联每个连接的指令的先后顺序,每个客户端的指令都会通过队列来排队进行顺序处理,先到先服务。
同样也会为每个客户端套接字关联一个响应队列。通过响应队列将指令的返回结果返回给客户端。如果队列为空,那么意味着socket暂时处于空闲状态,也就可以讲当前客户端描述符从write_fds里面移出来,等到队列有数据的时候再将描述符放进去。
定时任务
服务器除了响应I/O事件外,还要处理其他事情。比如定时任务。如果线程阻塞在select系统调度上的话,定时任务就会无法准时调度。那Redis是解决的呢?
最小堆, Redis的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。每个循环周期,Redis都会对最小堆里面已经到点的任务立即进行处理。然后将下一次最快要执行的任务还需要的时间记录下来,作为selctOS调度的timeout参数。然后安心的进行睡眠,因为这timeout的这段时间内不会有其他定时任务需要处理。 Nginx和Node.js的事件处理也是类似的。