Redis如何实现高性能?Redis,NOSQL数据库,在计算机界无人不晓。只要涉及到数据,就需要数据库。数据库的种类很多,但是NOSQLKV内存数据库也有很多。作为其中之一,redis是如何做到行业天花板的呢?如何实现高性能?如何实现高可用?今天在这篇八卦文章里,整理一些redis的设计,写一下。这篇文章仍然是关于高性能的。高效的数据结构与传统的关系型数据库相比,Redis数据库在数据结构上也有特殊之处。它所有的数据类型都可以看作是一个map结构,key作为查询条件。Redis是基于KV的内存数据库。它在内部构建了一个哈希表。根据指定的KEY访问时,只需要O(1)的时间复杂度就可以找到对应的数据,value的值是一些具有各种特征的。数据结构,为redis提供了良好的数据操作性能。与传统的基于内存存储的关系型数据库相比,数据文件可能以lsmtree或b+tree的形式存储在硬盘上。这时候读取文件需要IO操作,而redis是在内存中进行的,不会消耗大量的CPU资源。如此之快。从上图可以看出内存是介于硬盘和CPU缓存之间的。与硬盘相比,搜索数据肯定更快。当然在我个人看来,如果关系型数据库也把一些普通的数据库放在内存缓存中,也会得到一些性能上的提升,比如操作系统中的pagefault异常,通过一些特殊的方式将数据碎片缓存在内存中减少文件io开销的算法。io多路复用是传统的并发情况,如果一个进程不行,多个进程可以同时处理多个客户端连接吗?多进程可以解决部分并发问题,但仍然存在上下文切换开销、线程循环产生、PCB来回恢复效率低等问题。随着客户端请求数的增加,线程数也随着请求数线性增加。如果是并发,涉及到数据共享访问,有时会涉及到使用锁来控制范围的顺序,影响其他线程的执行效率。(一个进程在linux中也可以理解为一个线程,每个进程只有一个线程,当然我上面写的进程在这里,不用管它...)线程是运行在进程上下文中的逻辑流。一个进程可以包含多个线程,多个线程运行在同一个进程上下文中,因此进程地址空间的所有内容都可以共享,解决了进程间通信困难的问题,同时,由于上下文线程比进程的上下文小得多,线程的上下文切换比进程的上下文切换效率高得多。Redis和Nginx等应用程序是单线程程序。为什么他们能取得如此强劲的业绩?先看一个例子:午餐阻塞IO,我跟饭店老板说我要一碗‘热干面’,然后我就等着老板做出来,老板没有做出来好吧,所以我只是在那里等着什么。在“热干面”准备好之前不要这样做。这个过程就是我们常说的BlockingI/O如图所示:在同步阻塞IO模型中,应用程序发起read调用后,会一直阻塞,直到内核将数据拷贝到用户空间。NonBlockingIOSwitchtothecommon:同理,中午吃午饭,跟饭店老板说要一碗“热干面”,老板就开始做。你每隔几分钟就问老板,“你准备好了吗?”’,直到老板同意,你才拿到‘热干面’结束。在同步非阻塞IO模型中,应用程序会一直发起read调用,等待数据从内核空间复制到用户空间,直到数据复制到用户空间中线程仍然阻塞内核,并执行轮询操作,为了避免不断的阻塞,检索热干面的过程就是内核将准备好的数据交换给用户空间的过程。综上所述,以上两种模式的缺点是相似的。他们都是在等待内核准备数据,然后阻塞等待。阻塞的问题也是不可避免的。应用程序不断执行I/O系统调用,轮询数据是否就绪。非常占用CPU。I/OMultiplexing和前面的例子是一样的:午餐我让餐厅老板要一碗“热干面”,然后老板安排下面的厨师来做。我不知道哪个厨师会做。有几个厨师。有一段时间,我问下面的厨师准备好了吗。如果他们准备好了,他们会通知我来取餐。在IO多路复用模型中,线程首先发起select调用,询问内核数据是否就绪。内核准备好数据后,用户线程发起读取调用。read调用的进程(数据来自内核空间->用户空间)仍然阻塞。Reactor通过I/O多路复用程序监听客户端请求事件,接收到事件后通过任务调度器进行分发。对于连接建立请求事件,由Acceptor处理,建立相应的handler负责后续的业务处理。对于未连接事件,Reactor会调用相应的handler完成读->业务处理->写处理过程,并将结果返回给客户端,整个过程在一个线程中完成。Redis是基于Reactor单线程模式实现的。IO多路复用程序收到用户的请求后,将它们全部推入一个队列,交给文件调度器。对于后续的操作,在reactor单线程实现中看到,整个过程在一个线程中完成,所以Redis被称为单线程操作。我们通常说的Redis单线程快就是说它的请求处理过程非常快!在一个线程中监听多个Socket请求。当任意一个Socket可读/可写时,Redis读取客户端请求,操作内存中相应的数据,然后写回该Socket。单线程的好处:访问共享资源和加锁没有性能损失。开发调试非常友好,可维护。没有多线程上下文切换带来的额外开销。劣势也很明显。如果前面的请求耗时较长,整个Redis都会被阻塞,其他请求就进不来了,直到耗时操作完成返回,才能处理其他请求,但是redis使用的Reactor单线程模式可以缓解这种情况。在Redis4.0之后的版本中,引入了多线程,这种多线程只是异步释放内存。主要是解决大内存数据释放时整个redis都阻塞的性能问题。如果单机redis处理大数据请求,还是会存在瓶颈,但是redis有集群高可用的方案来解决。主节点只负责写,从节点负责读。这里先写IO多路复用。我将发布另一篇关于集群高可用性的文章。Copy-on-write有高效的数据结构和io多通道模型,目前可以解决数据访问效率的问题,但是redis有快照机制保证数据不丢失。说到快照,它可以操作磁盘。Redis是如何解决数据操作的?什么时候也能保证数据记录的完整性?不影响数据访问效率?答案是使用写时复制技术。什么是写时复制?如果你是专业人士或者你的操作系统学得很好,这个问题就很清楚了。在操作系统的设计中,进程的内存可以分为虚拟内存和物理内存。什么是虚拟内存?你可以看我上一篇文章VirtualMemory。Redis会从主进程通过fork()系统调用创建子进程,将父进程的虚拟内存和物理内存映射关系复制给子进程,并设置内存共享,子进程只负责将内存中的数据写入rdb进行持久化操作。如果主进程在运行过程中修改了内存,则使用copy-on-write技术创建相应内存的副本并写入持久化。如上图所示,主进程提供服务。只有当有人修改当前内存数据时,才会复制修改后的内存页,生成快照。管道通信影响客户端读写效率除了本地服务器内存和数据结构的操作外,还有网络原因。redis的通信协议使用的是文件协议。如果大家有兴趣自己研究,这里就不写了。客户端每次操作时,都会将命令和元数据打包成redis协议传输给服务端。这样按照每条命令的执行时间:客户端发送时间+服务器处理返回时间+一次网络往返时间。从上图可以看出,如果每条命令都进行操作,都要执行一个networkio。如果客户端频繁操作数据,就会进行频繁的网络操作。这个过程也很耗时,影响性能。Redis在客户端程序做了一些优化,引入了管道(pipelining)的概念。在一些批量操作数据的场景下,流水线会批量执行多个不相关的命令,以减少单独执行多个命令带来的网络交互时间。总结简单始于复杂!,别看客户端简单的api调用,背后还有很多值得学习的设计,看完这篇千篇一律的文章,你可能会对redis的高性能有一个新的认识,不要小看优化某些细节和解决方案选择有时可以带来显着的性能提升。当然,本文并没有说完redis的设计,比如aof的内核文件描述符映射、数据异步写入硬盘、零拷贝技术等等。...后续文章会更新redis如何高可用?
