Redis为什么这么快
总结起来主要有三点:1.纯内存结构。2.请求处理单线程。3.多路复用机制
1 原因
1.1 内存
KV结构的内存数据库,时间复杂度是O(1)。
1.2 单线程
这里说的单线程其实指的是处理客户端的请求是单线程的,可以把它叫做主线程。从4.0版本之后,还引入了一些线程处理其他的事情,比如清理脏数据,无用连接的释放,大key的删除。
把处理请求的主线程设置成单线程有什么好处呢?
1.没有创建线程,销毁线程带来的消耗
2.避免了上下文切换导致的CPU消耗
3.避免了线程之间带来的竞争关系,例如加锁释放锁死锁等等。
这里有个问题,就算单线程确实有这些好处,但是会不会白白浪费了CPU的资源?也就是说只能用到单核。官方的解释是这样的:在Redis中单线程已经够用了,CPU不是redis的瓶颈。redis的瓶颈最有可能是机器内存或者网络带宽,既然单线程容易实现,又不需要处理线程并发的问题,那就顺理成章的采用单线程的方案了。注意,因为请求处理是单线程的,不要在生产环境运行长命令,比如keys,flushall,flushdb。否则会导致请求被阻塞。
1.3 同步非阻塞IO
同步非阻塞IO,多路复用并发连接
2 单线程为什么这么快?
因为redis是基于内存的操作,先从内存开始说起
2.1 虚拟存储器(虚拟内存Virtual Memory)
计算机里面的内存叫做主存,硬盘叫做辅存。主存可看作一个很长的数组,一个字节一个单元,每个字节有一个唯一的地址,这个地址叫做物理地址(Physical Address)。早期的计算机中,如果CPU需要内存,使用物理寻址,直接访问主存储器。

这种方式有几个弊端:
1.一般的操作系统都是多用户多任务的,所有的进程共享主存。如果每个进程都独占一块物理地址空间,主存很快就会被用完。我们希望在不同的时刻,不同的进程共用同一块物理地址空间。
2.如果所有进程都是直接访问物理内存,那么一个进程就可以修改其他进程的内存数据,导致物理地址空间被破坏,程序运行就会出现异常。
所以想了一个办法,在CPU和主存之间增加一个中间层。CPU不再使用物理地址访问主存,而是访问一个虚拟地址,由这个中间层把地址转换成物理地址,最终获得数据。这个中间层叫做MMU(Memory Management Unit),内存管理单元。
具体的操作如下所示:

访问MMU就跟访问物理内存一样,所以把虚拟出来的地址叫做虚拟内存(Virtual Memory)。
在每一个进程开始创建的时候,都会分配一段虚拟地址,然后通过虚拟地址和物理地址的映射来获取真实数据,这样进程就不会直接接触到物理地址,甚至不知道自己调用的哪块物理地址的数据。目前,大多数操作系统都使用了虚拟内存,如Windows系统的虚拟内存,Linux系统的交换空间等。Windows的虚拟内存(pagefile.sys)是磁盘空间的一部分。在32位系统上,虚拟地址空间大小是2^32=4G。在64位系统上,最大虚拟地址空间大小是多少?是不是2^64-1024*1024TB?实际上没有用到64位,因为用不到这么大的空间,而且会造成很大的系统开销。linux一般用低48位来表示虚拟地址空间,也就是2^48=256T.
实际的物理内存可能远远小于虚拟内存的大小。总结:引入虚拟内存的作用:
1.通过把同一块物理内存映射到不同的虚拟地址空间实现内存共享。
2.对物理内存进行隔离,不同的进程操作互不影响
3.虚拟内存可以提供更大的地址空间,并且地址空间是连续的,使得程序编写,连接更加简单。
Linux/GNU的虚拟内存又进一步划分成了两块
2.2 用户空间和内核空间
一部分是内核空间(Kernel-space),一部分是用户空间(User-space)。

Linux系统中,虚拟地址布局如下:

进程的用户空间中存放的是用户程序的代码和数据,内核空间中存放的是内核代码和数据。不管内核空间还是用户空间,它们都处于虚拟内存空间中,都是对物理地址的映射。当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。进程在内核空间可以访问受保护的内存空间,也可以访问底层硬件设备。也就是可以执行任意命令,调用系统的一切资源。在用户空间只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称system call),才能向内核发出指令。所以,这样划分的目的是为了避免用户进程直接操作内核,保证内核安全。
2.3 进程切换(上下文切换)
多任务操作系统是怎么实现运行远大于CPU数量的任务个数的?当然,这些任务实际上并不是真的在同时运行,而是因为系统通过时间片算法,在很短的时间内,将CPU轮流分配给它们,造成多任务同时运行的错觉。在这个交替运行的过程中,为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,以及恢复以前挂起的某个进程的执行。这个行为被称为进程切换。
什么叫上下文(Context)?在每个任务运行前,CPU都需要知道任务从哪里加载,又从哪里开始运行,也就是说,需要系统事先帮它设置好CPU寄存器和程序计数器,这个叫做CPU的上下文。
而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
在切换上下文的时候,需要完成一系列的工作,这是一个很消耗资源的操作。
2.4 进程的阻塞
正在运行的进程由于提出系统服务请求(如IO操作),但因为某种原因未得到操作系统的立即响应,该进程只能把自己变成阻塞状态,等待响应的时间出现后才被唤醒。进程在阻塞状态不占用CPU资源。
2.5 文件描述符FD
Linus系统将所有设备都当作文件来处理,而Linux用文件描述符来标识每个文件对象。文件描述符(File Descriptior)是内核为了高效管理已被打开的文件锁创建的索引,用于指向被打开的文件,所有执行IO操作的系统调用都通过文件描述符。
文件描述符是一个简单的非负整数,用以标名每个被进程打开的文件。linux系统里面有三个标准文件描述符:
0:标准输入(键盘);1:标准输出(显示器);2:标准错误输出(显示器)。
2.6 传统IO数据拷贝
以读操作为例:但应用程序执行read系统调用读取文件描述符(FD)的时候,如果这块数据已经存在于用户进程的页内存中,就直接从内存中读取数据。如果数据不存在,则先将数据从磁盘加载数据到内核缓冲区中,再从内核缓冲器拷贝到用户进程的页内存中。(两个拷贝,两次user和kernel的上下文切换)。

2.7 Bocking I/O
当使用read或write对某个文件描述符进行过读写时,如果当前FD不可读,系统就不会对其他的操作做出响应,从硬件设备复制数据到内核缓冲区是阻塞的,从内核缓冲区拷贝到用户空间,也是阻塞的,知道copy complete,内核返回结果,用户进程才解除block的状态。

为了解决阻塞的问题,有几个思路
1.在服务端创建多个线程或者使用线程池,但是在高并发的情况下需要的线程会很多,系统无法承受,而且创建和释放线程都需要消耗资源。
2.由请求方定期轮询,在数据准备完毕后再从内核缓存区复制数据到用户空间(非阻塞IO),这种方式存在一定的延迟。
能不能用一个线程处理多个客户端请求?
2.8 I/O多路复用(IO Multiplexing)
IO指的是网络IO
多路指的是多个TCP连接(Socket或Channel)。
复用指的是复用一个或多个线程。
它的基本原理就是不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符。客户端再操作的时候,会产生具有不同事件类型的socket。在服务端,IO多路复用程序(IO Multiplexing Module)会把消息放入队列中,然后通过文件事件分派其(File event Dispatcher),转发到不同的事件处理器中。

多路复用有很多实现,以select为例,当用户进程调用了多路复用器,进程会被阻塞。内核会监视多路复用器负责的所有socket,当任何一个socket的数据准备好了,多路复用器就会返回。这时候用户进程再调用read操作,把数据从内核缓冲器拷贝到用户空间。

所以,IO多路复用的特点是通过一种机制让一个进程能同时等待多个文件描述符,而这些文件描述符其中的任意一个进入读就绪(readable)状态,select()函数就可以返回。多路复用需要操作系统的支持。redis的多路复用,提供了select,epoll,evport,kqueue几种选择,在编译的时候来选择一种。
evport是Solaris系统内核提供支持的;
epoll是Linux系统内核提供支持的;
kqueue是Mac系统提供支持的;
select是POSIX提供的,一般操作系统都有支撑(保底方案)
总结:redis抽象了一套AE事件模型,将IO事件和时间事件融入一起,同时借助多路复用机制的回调特性(Linux上用epoll),使得IO读写都是非阻塞的,实现高性能的网络处理能力。
一直在说的redis新版本多线程的特性,意思并不是服务端接收客户端请求变成多线程了,它还是单线程的。严格意义上来说,redis从4.0之后就引入了多线程用来处理一些耗时长的工作和后台工作,那不然的话,如果真的只有一个线程,那些耗时的操作肯定会导致金额护短请求被阻塞。我们这里说的多线程,确切的说叫做多线程IO
2.9 多线程IO
服务端的数据返回给客户端,需要从内核空间copy数据到用户空间,然后会写道socket(write调用),这个过程是非常耗时的。所以多线程IO指的就是把结果写道socket的这个环节是多线程的。处理请求依然是单线程的,所以不存在线程并发安全问题。

Redis本质上是一个存储系统。所有的存储系统在数据量过大的情况下都会面临存储瓶颈,包括MySQL,RabbitMQ等等。这里要解决两个问题:首先作为一个内存的KV系统,redis服务肯定不是无限制的使用内存,应该设置一个上限(max_memory)。第二个,数据应该有过期属性,这样就能清除不再使用的key。