1.为什么使用缓存技术?
Mysql和Oracle数据都是存放在磁盘中,虽然在数据库层中也做了对应缓存,但数据库层次的缓存一般仅针对于查询的内容。一般只有表中数据没有发生变更的时候,数据库中的cache才会发生作用。但这并不能减少业务系统对数据库产生的增、删、改、查的IO压力。因此缓存数据库应运而生,该技术实现了对热点数据的高速缓存,提高应用的响应速度,极大缓解后端数据库的压力 。
2.主流应用架构
一般为了提升性能都会在客户端和存储层之间添加一个缓存层。
(1)当客户端向后端发送请求的时候,会先去缓存中查,看看有没有相关的数据。
(2)如果有相关数据,就将数据直接返回,减轻了存储层的压力。
(3)如果缓存中没有相关数据。
(4)则穿透缓存,再去存储层中查询
(5)如果在存储层中找到相关数据,则将该数据回写回缓存层中,以便下次客户端再次请求同样数据时,可以快速的从缓存层返回。回写的过程就是所谓的回种。
(6)将结果返回至客户端,完成一次请求、响应的操作。
(7)当我们发现存储层挂掉时,或者没办法提供服务时,可以让客户端请求直接打在缓存层上。
(8)不管有没有数据都直接返回,在有损的情况下,对外提供服务。
3.缓存中间件--Memcache和Redis
Memcache:代码层次类似于hash。
- 支持简单的数据类型;
- 不支持数据持久化存储;
- 不支持主从;
- 不支持分片。
Redis:
- 数据类型丰富(支持set、list等类型);
- 支持数据磁盘持久化存储;
- 支持主从;
- 支持分片。
建议有持久化需求、或者对数据结构和处理有高级要求的应用,应选择redis。
其他简单的key-value存储,选择Memcache。
4.为什么redis能这么快?
100000+QPS(QPS即query per second,每秒内查询次数)。
- 完全基于内存。
绝大部分请求是存粹的内存操作,执行效率高。不会受到硬盘IO速度的限制,所以速度极快(采用单进程、单线程模型的key- value数据库);
- 数据结构简单,对数据操作也简单。
redis不使用表,他的数据库不会预定义或者强制去要求用户对redis存储的不同数据进行关联。因此性能相比于关系型数据库要高出不止一个量级。其存储结构就是键值对,类似于hashmap,hashmap的优势就是查找和操作时间的复杂度都是O(1);
- 采用单线程,单线程也能处理高并发请求,想多核也可启动多实例。
一般在面对多并发的请求时候,首先想到的是用多个线程来处理,将IO线程与业务线程分开,业务线程使用线程池,避免频繁创建和销毁线程,即便是一次请求阻塞了也不会影响到其他请求。为什么redis会反其道而为之,准确来说redis的单线程结构是指其主线程是单线程的,这里主线程包括IO事件的处理以及IO对应的相关请求的业务的处理,此外主线程还负责过期键的处理,复制协调、集群协调等等,这些除了IO事件之外的逻辑,会被封装成周期性的任务,由主线程周期性的处理,正因为采用单线程的设计,对于客户端的所有读写请求都有一个主线程串行的处理,因此多个客户端同时对一个键进行写操作的时候,就不会有并发的问题,避免了频繁的上下文切换和锁竞争,使得redis执行起来效率更高。
那么单线程可以处理高并发的请求吗?当然可以了,redis都实现了。
首先,并发并不是并行,并行性意味着服务器能够同时执行几个事情,具有多个计算单元。而并发性IO流,意味着能够让一个计算单元来处理来自多个客户端的流请求,redis使用单线程配合着IO多路复用,将能大幅度的提升性能。在多核CPU流行的今天,只有一个线程,只用一个核,多少让人感觉很浪费,并且是否没法让CPU利用其它的计算能力,其实大家大可不必担心,因为redis早已对相关问题进行验证,在实际的测试中发现,redis支持的QPS相当高,并且在QPS峰值时候CPU也并没有被跑满,只是由于网络等原因,导致并发处理量等不能进一步上升,因此CPU并不是制约redis的性能瓶颈,此外大家依旧可以在多核的服务器中启动多个redis实例,来利用多核的特性。大家需要注意的是,这里我们一直强调的单线程,只是在处理我们的网络请求的时候,只有一个单线程来处理。一个正式的redis server在运行的时候,肯定不止是一个线程的,例如redis进行持久化时候,会根据实际情况,以子进程或者子线程的方式执行。
- 使用多路I/O复用模型,非阻塞IO。
redis是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或者输出都是阻塞的,所以I/O操作,在一般情况下,往往不能直接返回,这会导致某一文件的I/O阻塞,进而导致整个进程无法对其它客户端提供服务,而I/O多路复用就是为了解决这个问题而出现的。
(1)FD:File Descriptor,文件描述符。
一个打开的文件通过唯一的描述符进行引用,该描述符是打开文件的源数据到文件本身的映射。
在Linux内核中,这个描述符称为文件描述符,文件描述符用一个整数来表示。
(2)传统的阻塞I/O模型
当使用read或者write对某一个文件描述符FD进行读写时,如果当前FD不可读,或者不可写,整个redis服务就不会对其它的操作做出响应,导致整个服务不可用,这也就是传统意义上的,也就是我们在编程中使用最多的阻塞模型。阻塞模型虽然在开发中非常常见,也非常易于理解,但是他会影响其它FD对应的服务,所以在需要处理多个客户端任务的时候,往往都不会使用阻塞模型,此时,需要一种更高效率的I/O模型来支持redis的高并发处理,这里涉及的就是I/O多路复用模型。
(3)Select系统调用--I/O多路复用模型中最终要的函数调用
select方法能够同时监控多个文件描述符的可读和可写操作,当其中的某些文件描述符可读、可写状态时,select方法就会返回可读、可写的文件描述符个数。select是负责监听文件是否可读、可写。监听的任务交给selector之后,程序又可以继续进行别的事情而不被阻塞了。
(4)多路I/O复用模型
Redis采用的I/O多路复用函数:epoll/kqueue/evport/select
epoll/kqueue/evport相比于select性能更为优秀,同时也能支撑更多的服务。
- 因地制宜(根据编译平台的不同);
- 优先选择时间复杂度为O(1)的I/O多路复用函数作为底层实现
包括Linux中的epoll和macOs中的kqueue等,上述的这些函数都使用了内核内部的结构,并且能够服务几十万的文件描述符,性能比select优秀 ;
- 以时间复杂度为O(n)的select作为保底
因为select函数是作为poxy标准中的系统调用,在不同版本的操作系统上都会实现,以此作为保底方案,因为在当前的编译环境中没有epoll/kqueue/evport等比select更优秀的函数,将会选择select作为备选方案。由于其在使用时会扫描全部的监听的描述符,因此其时间复杂度较差,通常为O(n)
- 基于react设计模式监听I/O事件。
使用react设计模式来实现文件事件处理器的,文件事件处理器使用I/O多路复用模块,同时监听多个FD。当accept、read、write和close文件事件产生时,文件事件处理器就会回调FD绑定的事件处理器。虽然整个文件事件处理器是在单线程上运行的,但是通过I/O多路复用模块的引用,实现了同时对多个FD读写的监控,提高了网络通信模型的性能,同时也可以保证整个redis服务实现的简单。