一、Redis 为什么选择单线程?
这种说法其实并不严谨,为什么这么说呢?
Redis的版本有很多 3.x
、4.x
、6.x
,版本不同架构也不同的,不限定版本问是否单线程也是不太严谨。
版本3.x,最早版本,也就是大家口口相传的redis的单线程,在3.x的版本是redis确实是单线程。
版本4.x,严格意义上来说也不是单线程,而是负责处理客户端请求的线程是单线程,但是开始加了多线程的东西(异步删除)。--并没有完全显现。
2020年5月版本的6.0x后及2022年推出的7.0版本后,告别了大家印象中的单线程,用一种全新的多线程来解决问题。---已经完全体现了。
里程碑的重要版本
5.0版本是直接升级到6.0版本,对于这次激进的升级,redis之父antirez表示得很有信心和兴奋,所以第一时间发文阐述了6.0的一些重大功能"Redis 6.0.0 GA is out!" ,当然,Redis7.0后版本更加厉害。
1.Redis单线程介绍
主要是指Redis
的网络IO的键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取(socket 读)、解析、执行、内容返回(socket 写)等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是 Redis对外提供键值存储服务的主要流程。
但是Redis的其他功能,比如持久化RDB、AOF、异步删除、集群数据同步等,其实是由额外的线程执行。
Redis命令工作线程是单线程的,但是,整个Redis来说是多线程的。
2.单线程为什么很快?
在Redis 3.x
单线程时代性能依旧很快的主要原因如下:
- 基于内存操作:
Redis
的所有数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能比较高。 - 数据结构简单:
Redis
的数据结构是专门设计的,而这些简单的数据结构的查找和操作的时间大部分复杂度都是O(1),因此性能比较高。 - 多路复用和非阻塞I/O:
Redis
使用epoll
和I/O多路复用
功能来监听多个socket
连接客户端,这样就可以使用一个线程来连接处理多个请求,减少线程切换带来的开销,同时也避免了I/O
阻塞操作。 - 避免上下文切换:因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生。
官方说明:
https://redis.io/docs/get-started/faq/
Redis是单线程的,如何利用多个CPU/内核?
CPU并不是使用Redis的瓶颈,因为通常Redis要么受内存限制,例如,使用在平均Linux系统上运行的流水线Redis每秒可以发送一百万个请求,因此,如果应用主要使用 O(N) 或者 (log (N) ) 命令,则几乎不会使用过多的CPU。
但是,为了最大成都利用CPU,可以在同一框中启动多个Redis实例,并将它们视为不同的服务器,在某个时候,单核可能还是不够,因此,如果需要使用多个CPU,则可以开始考虑更早地进行分片的某种方法。
但是,在Redis 4.0中,Redis具有更多线程,目前,这个仅限于后台删除对象,以及阻止通过Redis模块实现的命令,对于将来的版本,计划使用Redis越来越线程化。
大致意思就是说Redis是基于内存操作的,因此它的瓶颈可能是机器的内存或者网络带宽而不是CPU,既然CPU不是瓶颈,那么自然就采用单线程的解决方案了,况且使用多线程比较麻烦。但是在 Redis 4.0 中开始支持多线程了,例如后台删除、备份等功能。
Redis 4.0 之前一直采用单线程的主要原因有以下三个 :
- 使用单线程模型是Redis的开发和维护更加简单,因为单线程模型方便开发和调试。
- 即使使用单线程模型也可以并发的处理多客户端的请求,主要使用的是IO多路复用和非阻塞IO;
- 对于Redis系统来说,主要的性能瓶颈是内存或者网络带宽而非CPU。
3.Redis 为什么要加入多线程?
既然单线程这么好,为什么逐渐又要加入了多线程特性呢?
因为的单线程也存在一定的问题,如在执行复杂任务时,线程卡死等操作...
3.1 单线程的问题
正常情况下使用del
指令可以很快的删除数据,而当被删除的key是一个非常大的对象的时候,例如包含了成千上万的元素 hash 集合时,那么del
指令就会造成 Redis主线程卡顿等问题。
这就是 redis3.x
单线程时代最经典的故障,大key删除的头疼的问题。
由于Redis
是单线程的,del bigKey ...
等待很久这个线程才会释放,类似加了一个synchronized
锁,你可以详细高并发下,线程堵塞导致系统崩溃无法使用。
3.2 解决方法
使用惰性删除可以有效的避免Redis
卡顿的问题,因为在使用del key
是同步操作,则可以使用异步的操作解决这个问题。
比如当需要删除一个很大的数据时,因为是单线程原子命令操作,这就会导致Redis服务卡顿,于是在Redis4.0 中新增了多线程的模块,当然次版本中多线程主要是为了解决删除数据效率比较低的问。
unlink key
flushdb async
flushall async
包删除的工作交给后台的子线程来完成异步删除数据。
因为Redis是单主线程处理,redis支付antirez一直强调"Lazy Redis is better Redis"。
而lazy free
的本质就是把某些cost
(主要时间复制度,占用主线程cpu)较高删除操作,而redis主线程剥离让bio子线程来处理,极大地减少主线程阻塞时间。从而减少删除导致性能和稳定性问题。
在Redis 4.0
就引入了多线程来实现数据的异步惰性删除等功能,但是其实读写请求仍然只有一个线程,所以仍然算是狭义上的单线程。
二、Redis6/7的多线程特性和IO多路复用
对于Redis主要的性能瓶颈是内存或者网络带宽而非CPU。
1.网络瓶颈
在Redis6/7中,非常收关注的第一个新特性就是多线程。
这是因为,Redis一直被大家熟知的就是它的单线程架构,虽然有些命令操作可以用后台线程或者子线程执行(比如数据删除、快照生成、AOF重写)。但是,从网络IO处理到实际的读写命令处理,都是由单个线程完成的。
随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络IO的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度,为了应对这个问题:
采用多个IO线程来处理网络请求,提高网络请求处理的并行度,Redis6/7就是采用的这种方法。
但是,Redis的多IO线程只是用来处理网络请求的,对于读写操作命令Redis仍然使用单线程来处理。这是因为,Redis处理请求时,网络处理经常出现瓶颈,通过多个IO线程并行处理网络操作,可以提升实例的整体处理性能。而继续使用单线程执行命令操作,就不用为了保证Lua脚本
、事务的原子性
、额外开发多线程互斥加锁机制了(不管加锁操作处理),这样一来,Redis线程模型实现就简单了。
2.主线程和IO线程协作
阶段一:服务端和客户端建立Socket连接,并分配处理线程
首先,主线程负责接收建立连接请求,当有客户端请求和实例建立Socket连接时,主线程会创建和客户端的连接,并把Socket放入到全局等待队列中,紧接着,主线程通过轮询方式把Socket连接分配给IO线程。注意:主线程不一定会将连接直接分配给IO线程,而是通过 epoll
或者其他I/O多路复用机制
,主线程会监听多个连接,一旦有事件发生,比如有数据可读,主线程会通知对应的IO线程去处理,此外Redis可能会用线程来处理这些IO任务,而不是一对一地将连接分配给IO线程。
阶段二:IO线程读取并解析请求
主线程一旦把Socket分配给IO线程,就会进入阻塞状态,等待IO线程完成客户端请求读取解析。因为有多个IO线程在并行处理,所以,这个过程很快就可以完成。
阶段三:主线程执行请求操作
等待IO线程解析完请求,主线程还是会以单线程的方式执行这些命令操作。
阶段四:IO线程回写Socket和主线程清空全局队列
当主线程执行完请求操作后,会把需要返回的结果写入到缓冲区,然后,主线程会阻塞等待IO线程,把这些结果写回到Socket中,并返回给客户端。和IO线程读取和解析请求一样,IO线程回写Socket时,也是有多个线程在并发执行,所以回写Socket的速度也很快。等到IO线程回写Socket完毕,主线程会清空全局队列,等待客户端的后续请求。
3.Unix网络中五种IO模型
- Blocking IO - 阻塞IO
- NoneBlocking IO - 非阻塞IO
- IO multplexing - IO多路复用
- signal driven IO - 信号驱动IO
- asynchronous IO - 异步IO
IO multplexing - IO 多路复用
:
在Linux
世界中一切皆文件:文件描述符:简称FD,句柄
File descriptor
:文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引入的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,文件描述符这一概念往往只适用于UNIX
、Linux
这样的操作系统。
4.IO多路复用
一种同步的IO
模型,实现一个线程监视多个文件句柄,一旦某个文件句柄就绪就能够通知到对应应用程序进行响应的读写操作,没有文件句柄就绪时会阻塞应用程序,从而释放CPU资源。
总而言之就是:I/O多路复用是一种高效的I/O操作机制,用于同时监视多个I/O流的可读性、可写性或异常情况,而无需为每个I/O流分配一个单独的线程或进程。这种技术允许一个单独的线程或进程来管理多个I/O操作,从而减少了系统资源的消耗并提高了系统的响应性和吞吐量。
4.1 概念
I/O
:网络I/O,尤其在操作系统层面指数据在内核态和用户态之间的读写操作。多路
:多个客户端连接(连接就是套接字描述符,即socket 或者 channel)复用
:复用一个或几个线程。IO多路复用
:也就是说一个或一组线程处理多个TCP连接,使用单进程就能够实现同时处理多个客户端的连接,无需创建或者维护过多的进程/线程。
一个服务端进程可以同时处理多个套接字描述符,实现IO多路复用
的模型有三种:可以分select -> poll ->epoll
三个阶段来描述。
select()
:select() 是一种比较早的I/O
多路复用机制,它允许程序员监视一组文件描述符,并在其中任何一个文件描述符就绪时进行通知。但是select()
有一些限制,比如它通常有最大文件描述符数量的限制,并且在大量文件描述符的情况下性能较差。
poll()
:poll() 是对select()
的改进,它也可以同时监视多个文件描述符,但没有select()
的一些限制,poll()
使用一个pollfd
结构数组来传递需要监视的文件描述符和所关注的事件。
epoll
:epoll()是Linux
特有的I/O
多路复用机制,它提供了更高效的时间通知机制。相比较select()
和poll()
、epoll()
在处理大量文件描述符时具有更好的性能。epoll()
使用一个epoll
实例来管理需要监视的文件描述符,并通过epoll_ctl()
来添加、删除或者修改监视的文件描述符。然后通过epoll_wait()
来等待就绪时间的发送。
I/O
多路复用的优势在于,它避免了创建多个线程或者进程来处理I/O
操作,从而减少了上下文切换的开销,提高了系统的性能和资源利用率,这种技术常用于网络编程中,用于处理大量并发连接的情况,例如WEB服务器、聊天服务器等。
4.2 IO多路复用模型
场景分析:
模拟一个tcp
服务器处理30个客户端socket
,假设你是一个监考老师,让30个学生解答一道竞赛考题,然后负责验收学生答卷,你有下面几个选择:
第一种选择(轮询):按照顺序逐个验收,先验收A、然后是B,之后是C、D。。。这中间如果有一个学生卡主了,全班都会被耽误,你用循环挨个处理socket
,根本不具有并发能力。
第二种选择(来一个new一个,一对一服务):你创建30个分身线程,每个分身线程检查一个学生的答案是否正确。这种类似于为每个用户创建一个进程或者线程处理连接。
第三种选择(响应式处理,1对多服务):你站在讲台上等,谁解答完谁举手。这时C、D
举手,表示他们解答问题完毕,你下去依次检查C、D
的答案,然后继续回到讲台上等。此时E、A
又举手,然后去处理E和A。。。
这种就是IO复用模型
。Linux
下的select
、poll
和epoll
就是干这个的。
IO多路复用模型:
将用户socket
对应的文件描述符(FileDescriptor)注册进epoll
,然后epoll
帮你监听哪些socket
上有消息到达,这样就避免了大量的无用操作。此时的socket
应该采用非阻塞模式
。这样,整个过程只在调用select
、poll
、epoll
这些调用的时候才会堵塞,收发客户消息是不会阻塞的,整个进程或线程就被充分利用起来,这就是事件驱动,所谓的reactor
反应模式。
在单个线程通过记录跟踪每一个Socket(I/O流)
的状态俩同时管理多个I/O流
,一个服务端进程可以同时处理多个套接字描述符。目的是尽量的提高服务器的吞吐能力。
大家都用过nignx
,nginx
使用epoll
接收请求,nginx
会有很多请求过来,epoll
会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。redis类似同理,这就是I/O多路复用
原理,有请求就响应,没有请求不打扰。
4.3 Reactor模型
Redis服务采用Reactor
的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)
Redis基于Reactor
模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:
多个套接字、IO多路复用程序、文件时间分配器、事件处理器,因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
基于I/O
复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从线程阻塞状态返回,开始进行业务处理。
Reactor
模式,是指通过一个或者多个输入同时传递给服务器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应处理线程,Reactor
模式也叫Dispatcher
模式,即I/O多路复用
统一监听事件,收到事件后分发(Dispatch 给某进程),是编写高性能网络服务器的必备技术。
Reactor模式中有2个关键组成:
- Reactor:Reactor在独立的线程中运行,负责监听和分发事件,分发给适当的处理程序对IO事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线程转移到适当的联系人;
- Handlers:处理程序执行
I/O
事件要完成的实际事件,类似于客户想要与之交谈的公司的实际办理人。Reactor
通过调度适当的处理程序来响应I/O
事件,处理程序执行非阻塞操作。
三、结论
Redis
工作线程是单线程的,但是对于整个Redis
来说是多线程的。
I/O
的读和写本身就是堵塞的,比如当scoket
中有数据时,Redis
会通过调用先将数据从内核空间拷贝到用户态空间,再交给Redis
调用,而这个拷贝的过程就是阻塞的,当数量越大时拷贝所需要的时间就越多,这些操作都是基于单线程完成的。
从Redis6
开始,就新增了多线程的功能来提高I/O
的读写性能,他的主要实现思路是将主线程的IO读写任务拆分给一组独立的线程去执行,这样就可以使多个socket
的读写可以并行化了,采用多路I/O复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),将最耗时的Scoket
的读取、请求解析、写入单独外包出去,剩下的命令执行让然由主线程串行执行并和内存的数据交互。
结合上图可知,网络IO操作就变成多线程化了,其他核心部分仍然是线程安全的,是一个不错的折中办法。
Redis6 --> 7
将网络数据读写、请求协议解析通过多个IO线程来处理,对于真正的命令执行来说,仍然使用主线程操作,一举两得。
22