面试题
redis到底是单线程还是多线程?
lO多路复用听说过吗?
redis为什么快?
1 Redis重要版本历程
可以看到
4.0之前Redis使用的是纯粹的单线程方案。
4.0开始部分的支持多线程。
到如今的7.0可以说是全面支持了多线程,然而在键值读写这一方面,redis仍然使用着单线程的方式。
2 Redis4.0之前的单线程
Redis的单线程
主要是指Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是Redis对外提供键值存储服务的主要流程。
整体的来看,Redis的其他功能,比如持久化RDB、AOF、异步删除、集群数据同步等等,其实是由额外的线程执行的。
Redis命令工作线程是单线程的,但是,整个Redis来说,是多线程的;
因此,早期版本的redis,对于键值读写来说是纯粹的单线程。
那为什么单线程还能有这么快的速度呢?上面这个处理过程中,执行操作是的确需要单线程阻塞的进行,毕竟redis容易面对高并发的场景,读写操作的时候不阻塞,是十分危险的。因此矛头转向了读取socket,解析请求,写入socket这三个步骤。使用了io多路复用技术。
为什么 Redis 中要使用 I/O 多路复用这种技术呢?
因为 Redis 是跑在「单线程」中的,所有的操作都是按照顺序线性执行的,但是「由于读写操作等待用户输入 或 输出都是阻塞的」,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务。而 I/O 多路复用就是为了解决这个问题而出现的。「为了让单线程(进程)的服务端应用同时处理多个客户端的事件,Redis 采用了 IO 多路复用机制。
这里多路【指的是多个网络连接客户端】;复用【指的是复用同一个线程(单进程)】;
I/O 多路复用:其实是使用一个线程来检查多个 Socket 的就绪状态,在单个线程中通过记录跟踪每一个 socket(I/O流)的状态来管理处理多个 I/O 流。
IO多路复用的介绍
1.linux世界里,一切皆是文件,redis服务被多个客户端连接时,会用不同的文件符(称为FD,文件描述符)来描述这些客户端。多个客户端连接服务端时,Redis会将客户端socket对应的fd注册进epoll,然后epoll同时监听多个文件描述符(FD)是否有数据到来,如果有数据来了就通知事件处理器赶紧处理,这样就不会存在服务端一直等待某个客户端给数据的情形。
2.同时处理多个客户端请求:Redis服务器可能会同时接收来自多个客户端的请求,通过IO多路复用,它可以高效地管理和处理这些请求,而无需为每个客户端连接启动一个单独的线程。
当有某个客户端发起请求时,就通过FD找到该客户端,处理该客户端的请求。处理请求是非阻塞的,也就是可以同时处理多个请求。
3.非阻塞操作:通过IO多路复用,Redis可以实现非阻塞的网络通信,即使某个客户端的请求未就绪,Redis也可以继续处理其他客户端的请求,而不会因为等待而被阻塞。
4.事件驱动机制:IO多路复用使得Redis可以基于事件驱动的方式进行网络请求处理,提高了系统的响应速度和并发处理能力。
总结:
整个redis实际是多线程的。除了主线程,有时会有RDB,AOF,异步删除等其他线程在工作。但对于redis的工作线程来说是单线程的。
redis的工作线程(主线程)这一个线程内,维护着io多路复用,可以并发的处理工作流程中的网络io部分,而键值读写部分,仍然是单线程。(读取socket,解析socket,写回socket)
3 Redis4.0之前是单线程,为什么能这么快?
1)基于内存操作,Redis的所有数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能比较高。
2)数据结构简单:Redis的数据结构都是专门设计的,而这些简单的数据结构的查找和操作的时间大部分复杂度都是O(1),因此性能比较强,可以参考我之前写过的这篇文章。
3)多路复用和非阻塞I/O,Redis使用I/O多路复用来监听多个socket链接客户端,这样就可以使用一个线程链接来处理多个请求,减少线程切换带来的开销,同时也避免了I/O阻塞操作。
4)主线程为单线程,避免上下文切换,因为是单线程模型,因此避免了不必要的上下文切换和多线程竞争(比如锁),这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生。
4 Redis4.0多线程初见模型
正常情况下使用 del 指令可以很快的删除数据,而当被删除的 key 是一个非常大的对象时,例如时包含了成千上万个元素的 hash 集合时,那么 del 指令就会造成 Redis 主线程卡顿。
这就是redis3.x单线程时代最经典的故障,大key删除的头疼问题,
由于redis是单线程的,del bigKey .....
这个操作会阻塞redis主线程不少时间,于是乎,4.0引入了unlink删除
解决方法
比如当我(Redis)需要删除一个很大的数据时,因为是单线程原子命令操作,这就会导致 Redis 服务卡顿,
于是在 Redis 4.0 中就新增了多线程的模块,当然此版本中的多线程主要是为了解决删除数据效率比较低的问题的。
unlink key |
flushdb async |
flushall async |
因为Redis是单个主线程处理,redis之父antirez一直强调"Lazy Redis is better Redis".
而lazy free的本质就是把某些cost(主要时间复制度,占用主线程cpu时间片)较高删除操作,
从redis主线程剥离让bio子线程来处理,极大地减少主线阻塞时间。从而减少删除导致性能和稳定性问题。
5 Redis6/7IO多路复用和多线程特性全面使用
Redis的性能的影响因素:
如今时代,cpu和内存,redis往往都占不满,不是主要因素,所以主要的影响因素就是网络io这一方面。
在Redis6/7中,非常受关注的第一个新特性就是多线程。
这是因为,Redis一直被大家熟知的就是它的单线程架构,虽然有些命令操作可以用后台线程或子进程执行(比如数据删除、快照生成、AOF重写)。但是,从网络IO处理到实际的读写命令处理,都是由单个线程完成的。
随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络IO的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度,
为了应对这个问题:
采用多个IO线程来处理网络请求,提高网络请求处理的并行度,Redis6/7就是采用的这种方法。
但是,Redis的多IO线程只是用来处理网络请求的,对于读写操作命令Redis仍然使用单线程来处理。这是因为,Redis处理请求时,网络处理经常是瓶颈,通过多个IO线程并行处理网络操作,可以提升实例的整体处理性能。而继续使用单线程执行命令操作,就不用为了保证Lua脚本、事务的原子性,额外开发多线程互斥加锁机制了(不管加锁操作处理),这样一来,Redis线程模型实现就简单了。
执行流程
1. 客户端连接阶段
- 客户端连接请求被网络I/O线程接受和建立连接。
- 一旦连接建立,将会生成相应的套接字描述符,由网络I/O线程进行监视。
2. 命令处理阶段
- 当客户端发送命令时,网络I/O线程负责读取命令并将其传递给主线程。
- 主线程接收到命令后,会根据命令的类型和参数执行相应的命令处理逻辑。
3. 数据读写和命令处理交互
- 如果执行的命令需要读取数据(例如GET操作)或写入数据(例如SET操作),主线程会将相应的读写请求交给网络I/O线程执行,以便进行实际的数据传输。
4. 响应返回阶段
- 当主线程完成命令处理并生成响应后,将响应结果返回给网络I/O线程。
- 网络I/O线程负责将响应发送回客户端,并确保数据的正确传输。
线程通信与同步
在这个执行流程中,网络I/O线程和主线程之间需要进行良好的线程通信与同步。这涉及到数据的交换、互斥操作、条件变量的使用等,以确保线程之间的协作和数据的一致性。
图解
6 Redis7默认开启了多线程吗?
redis7默认没有开启
如果你在实际应用中,发现Redis实例的CPU开销不大但吞吐量却没有提升,可以考虑便用Redis7的多线程机制,加速网终处理,进而提升实例的吞吐量。
1.设置io-thread-do-reads配置项为yes,表示启动多线程。
2。设置线程个数。关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。
7 总结
redis到底是单线程还是多线程?
答:直接回答redis是单线程或是多线程,都是不严谨的。这就好比直接回答java中有泛型(jdk5才有),和Java中有lambda表达式(jdk8才有)一样不严谨。
Redis4之前,redis是单线程的。
Redis4开始逐渐的引入部分多线程。(比如异步删除)
Redis6/7全面支持了多线程,主线程中也有了多路的io复用。
lO多路复用听说过吗?答:多路【指的是多个网络连接客户端】;复用【指的是复用同一个线程(单进程)】
其实是使用一个线程来检查多个 Socket 的就绪状态,在单个线程中通过记录跟踪每一个 socket(I/O流)的状态来管理处理多个 I/O 流。
redis为什么快?答:
1)Redis是基于内存操作的
2)数据结构简单:Redis的数据结构的查找和操作的时间大部分复杂度都是O(1)
3)多路复用和非阻塞I/O,Redis使用I/O多路复用来监听多个socket链接客户端,这样就可以使用一个线程链接来处理多个请求,减少线程切换带来的开销,同时也避免了I/O阻塞操作。redis67还支持多线程的网络io
4)主线程为单线程,避免上下文切换,因为是单线程模型,因此避免了不必要的上下文切换和多线程竞争(比如锁),这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生