概述
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、非阻塞IO多路复用机制
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
基于内存存储
Redis 是在内存中进行键值存储。
Redis 中的每次读写操作都相当于从内存的变量中进行读写。
访问内存比直接访问磁盘快几个数量级,因此Redis 比其他数据存储快得多。
优化的数据结构
作为内存数据存储,Redis 利用各种底层数据结构来高效存储数据,无需担心如何将它们持久化到持久存储中。
例如,Redis list 是使用链表实现的,它允许在列表的头部和尾部附近进行恒定时间 O(1) 插入和删除。
另一方面,Redis sorted set 是通过跳跃列表实现的,可以实现更快的查询和插入。
简而言之,无需担心数据持久化,Redis 中的数据可以更高效地存储,以便通过不同的数据结构进行快速检索。
单线程
Redis 中的写入和读取速度非常快,并且 CPU 使用率从来不是 Redis 关心的问题。
根据 Redis 官方文档,在普通 Linux 系统上运行时,Redis 每秒最多可以处理 100 万个请求。
通常瓶颈来自于网络 I/O, Redis 中的处理时间大部分浪费在等待网络 I/O 上。
虽然多线程架构允许应用程序通过上下文切换并发处理任务,但这对 Redis 的性能增益很小,因为大多数线程最终会在 I/O 中被阻塞。
所以 Redis 采用单线程架构,有如下好处
● 最大限度地减少由于线程创建或销毁而产生的 CPU 消耗
● 最大限度地减少上下文切换造成的 CPU 消耗
● 减少锁开销,因为多线程应用程序需要锁来进行线程同步,而这容易出现错误
● 能够使用各种“线程不安全”命令,例如 Lpush
非阻塞IO多路复用机制
Redis采用epoll做为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接,读写,关闭
都转换为了事件,不在I/O上浪费过多的时间,使得Redis在网络 IO 操作中能并发处理大量的客户端请求,实现了高吞吐率。
为了处理传入的请求,服务器需要在套接字上执行系统调用,以将数据从网络缓冲区读取到用户空间。
这通常是阻塞操作,线程被阻塞并且在完全接收到来自客户端的数据之前不能执行任何操作。
为什么我们不能在只有确定套接字中的数据已准备好读取时,才执行系统调用嘞?
这就是 I/O 多路复用发挥作用的地方。
I/O 多路复用模块同时监视多个套接字,并且仅返回可读的套接字。
准备读取的套接字被推送到单线程事件循环,并由相应的处理程序使用响应式模型进行处理。
总之,
● 网络 I/O 速度很慢,因为其阻塞特性,
● Redis 收到命令后可以快速执行,因为这在内存中执行,操作速度很快,
所以 Redis 做出了以下决定,
● 使用 I/O 多路复用来缓解网络 I/O 缓慢问题
(1)多路 I/O 复用模型
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量
多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用用epoll作为I/O多路复用技术的实现。并且,Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
● I/O :网络 I/O
● 多路 :多个网络连接
● 复用:复用同一个线程。
● IO多路复用其实就是一种同步IO模型,它实现了一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;而没有文件句柄就绪时,就会阻塞应用程序,交出cpu。
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型
文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件,虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性
IO 多路复用机制
IO多路复用是一种同步的IO模型。利用IO多路复用模型,可以实现一个线程监视多个文件句柄;一旦某个文件
句柄就绪,就能够通知到对应应用程序进行相应的读写操作;没有文件句柄就绪时就会阻塞应用程序,从而释放出CPU资源。
IO可以理解为,在操作系统中,数据在内核态和用户态之间的读、写操作,大部分情况下是指网络IO多路大部分情况下是指多个TCP连接,也就是多个Socket 或者多个Channel;
2 select模型
select模型,它的基本原理是,采用轮询和遍历的方式。也就是说,在客户端操作服务器时,会创建三种文件描
述符,简称FD。分别是writefds(写描述符)、readfds(读描述符)和 exceptfds(异常描述符)。
而select会阻塞监视这三种文件描述符,等有数据、可读、可写、出异常或超时都会返回;
返回后通过遍历fdset,也就是文件描述符的集合,来找到就绪的FD,然后,触发相应的IO操作。
它的优点是跨平台支持性好,几乎在所有的平台上支持。
它的缺点也很明显,由于select是采用轮询的方式进行全盘扫描,因此,随着FD数量增多而导致性能下降。
因此,每次调用select()方法,都需要把FD集合从用户态拷贝到内核态,并进行遍历。而操作系统对单个进程打
开的FD数量是有限制的,一般默认是1024个。虽然,可以通过操作系统的宏定义FD_SETSIZE修改最大FD数量限制,但是,在IO吞吐量巨大的情况下,效率提升仍然有限。
3 poll模型
poll 模型的原理与select模型基本一致,也是采用轮询加遍历,唯一的区别就是 poll 采用链表的方式来存储FD。
所以,它的优点点是没有最大FD的数量限制。
它的缺点和select一样,也是采用轮询方式全盘扫描,同样也会随着FD数量增多而导致性能下降。
4 epoll模型
由于select和poll都会因为吞吐量增加而导致性能下降,因此,才出现了epoll模型。
epoll模型是采用时间通知机制来触发相关的IO操作。它没有FD个数限制,而且从用户态拷贝到内核态只需要一
次。它主要通过系统底层的函数来注册、激活FD,从而触发相关的 IO 操作,这样大大提高了性能。主要是通过调用以下三个系统函数:
1、epoll_create()函数,在系统启动时,会在Linux内核里面申请一个B+树结构的文件系统,然后,返回epoll
对象,也是一个FD。
2、epoll_ctl()函数,每新建一个连接的时候,会同步更新epoll对象中的FD,并且绑定一个 callback回调函数。
3、epoll_wait()函数,轮询所有的callback集合,并触发对应的 IO 操作
所以,epoll模型最大的优点是将轮询改成了回调,大大提高了CPU执行效率,也不会随FD数量的增加而导致效
率下降。当然,它也没有FD数量限制,也就是说,它能支持的FD上限是操作系统的最大文件句柄数。一般而言,1G内存大概支持 10 万个句柄。分布式系统中常用的组件如Redis、Nginx都是优先采用epoll模型。
它的缺点是只能在Linux下工作。
Redis是单线程模型和多线程
Redis6.0,它的执行命令操作内存的仍然是个单线程
单线程模型
Redis是单线程模型的,而单线程避免了CPU不必要的上下文切换和竞争锁的消耗。也正因为是单线程,如果某个命令执行过长(如hgetall命令),会造成阻塞。Redis是面向快速执行场景的数据库。,所以要慎用smembers和lrange、hgetall等命令。
Redis 6.0 引入了多线程提速,它的执行命令操作内存的仍然是个单线程。
Redis6.0之前是单线程的,Redis6.0之后开始支持多线程;
redis内部使用了基于epoll的多路服用,也可以多部署几个redis服务器解决单线程的问题;
redis主要的性能瓶颈是内存和网络;
内存好说,加内存条就行了,而网络才是大麻烦,所以redis6内存好说,加内存条就行了;
而网络才是大麻烦,所以redis6.0引入了多线程的概念,
redis6.0在网络IO处理方面引入了多线程,如网络数据的读写和协议解析等,需要注意的是,执行命令的核心模块还是单线程的
Redis6.0之前,Redis在处理客户端的请求时,包括读socket、解析、执行、写socket等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。
Redis6.0之前为什么一直不使用多线程?使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于内存和网络。例如在一个普通的Linux系统上,Redis通过使用pipelining每秒可以处理100万个请求,所以如果应用程序主要使用O(N)或O(log(N))的命令,它几乎不会占用太多CPU。
redis使用多线程并非是完全摒弃单线程,redis还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。
这样做的目的是因为redis的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。
为什么Redis 6.0+ 引入多线程
● Redis 3.x 版本是单线程的,存在 大key 删除问题;
● Redis 4.x 版本支持了部分多线程操作,主要是为了解决 3.x 的大key操作导致的超时性能问题;
● Redis 6.x 版本是多线程的。目的是降低网络IO的时间消耗
Redis 6.0 引入了一些新特性,其中非常受关注的一个特性就是多线程。在 4.0 之前 Redis 是单线程的,因为单线程的优点很明显,不但降低了 Redis 内部实现的复杂性,也让所有操作都可以在无锁的情况下进行,并且不存在死锁和线程切换带来的性能以及时间上的消耗。但是其缺点也很明显,单线程机制导致 Redis 的 QPS(Query Per Second,每秒查询数)很难得到有效的提高(虽然已经够快了,但人毕竟还是要有更高的追求的)。
而 Redis 从 4.0 版本开始引入了多线程,但是此版本的多线程主要用于大数据量的异步删除,对于非删除操作的意义并不是很大。
但 Redis 6.0 中的多线程则是真正为了提高 I/O 的读写性能而引入的,它的主要实现思路是将主线程的 I/O 读写任务拆分给一组独立的子线程去执行,也就是说从 socket 中读数据和写数据不再由主线程负责,而是交给了多个子线程,这样就可以使多个 socket 的读写并行化了。这么做的原因就在于,虽然在 Redis 中使用了 I/O 多路复用和非阻塞 I/O,但我们知道数据在内核态空间和用户态空间之间的拷贝是无法避免的,而数据的拷贝这一步是阻塞的,并且当数据量越大时拷贝所需要的时间就越多。所以 Redis 在 6.0 引入了多线程,用于分摊同步读写 I/O 压力,从而提升 Redis 的 QPS。但是注意:Redis 的命令本身依旧是由 Redis 主线程串行执行的,只不过具体的读写操作交给独立的子线程去执行了(后面会详细说明 Redis 的主线程和子线程之间是如何协同的),而这么做的好处就是不需要为 Lua 脚本、事务的原子性而额外开发多线程互斥机制,这样一来 Redis 的线程模型实现起来就简单多了。因为和之前一样,所有的命令依旧是由主线程串行执行的,只不过具体的读写任务交给了子线程。