Redis使用单线程以及多路复用机制解析

Redis使用单线程以及多路复用机制解析

一 Redis为什么使用单线程而不是多线程

正如redis官网上说,对于一个 DB 来说,CPU 通常不会是瓶颈,因为大多数请求不会是 CPU 密集型的,而是 I/O 密集型。具体到Redis 的话,如果不考虑 RDB/AOF 等持久化方案,Redis是完全的纯内存操作,所以相比存储在磁盘里的mysql等执行速度要快得多的多,因此这部分cpu操作通常不会是性能瓶颈。Redis 真正的性能瓶颈在于网络 I/O,也就是客户端和服务端之间的网络传输延迟,因此 Redis 6版本前选择了单线程的 I/O 多路复用来实现它的核心网络模型

选择单线程的原因:
1.避免过多的上下文切换,浪费不必要的时间:使用多线程难免涉及到线程切换,每次切换都需要记录线程运行时的状态,方便下次线程接着之前的状态启动。CPU在切换线程的时候,有一个上下文切换时间,而这个上下文切换时间是非常耗时的。 比如一个CPU主频是 2.6GHz,这意味着每秒可以执行:2.6*10^9 个指令,换算每个指令的时间大概是0.38ns,而一次上下文切换,将近需要耗时2000ns。而这个时间内,CPU什么都干不了,只是做了保存上下文都动作!

2.避免同步加锁机制的开销:
如果选择多线程,势必会涉及到底层数据同步的问题,这时可能会引入某些同步机制,比如锁,但是我们知道Redis不仅仅提供了简 单的 key-value 数据结构,还有 list、set 和 hash 等等其他丰富的数据结构,而不同的数据结构对同步访问的加锁粒度又不尽相同, 可能会导致在操作数据过程中带来很多加锁解锁的开销,增加程序复杂度的同时还会降低性能
3. 多线程的目的,就是通过并发的方式来提升I/O的利用率和CPU的利用率,但是Redis是基于内存的,CPU资源不是Redis的性能瓶颈。多线程确实可以提升效率,原因是I/O操作可以分为两个阶段:即等待I/O准备就绪和真正操作I/O资源,在等待就绪阶段,线程是在“阻塞”着等待磁盘。但是提升I/O利用率,并不是只有采用多线程技术这一条路可以走,Redis底层是基于IO多路复用来实现的

二 Redis并非只是单线程

我们所说的Redis单线程,指的是"其网络IO和键值对读写是由一个线程完成的",Redis是基于Reactor模式(底层是IO多路复用,可以理解为事件分发)开发的网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。但是,Redis实例还有其它线程。

请添加图片描述

Redis 4.0 开始就有多线程的概念了,比如 Redis 通过多线程方式在后台删除对象、以及通过 Redis 模块实现的阻塞命令等。

同时,Redis6.0开始针对数据包的接收和处理使用了多线程,核心的redis操作命令依然保持单线程。

三、阻塞IO

100%弄明白的5种io模型

在 socket 连接中,一个服务器进程和一个客户端进行通信时,当一个 client 端向服务端写数据时,如果 client 端没有发送数据,那么服务端的 read 将一直阻塞,直到客户端 write 发来数据。在一个客户和服务器通信时没什么问题,当多个客户 与 一个服务器通信时,就存在问题了。如下图,两个客服端同时连接一个服务端进行写数据的时序图

请添加图片描述

从上图可以看出一个服务器进程和多个客户端进程通信的问题:

  • (1) client1 和服务端建立连接后,服务端会一直阻塞于 client1,直到 client1 客户端 write 发来数据才开始后面的操作。服务端阻塞期间,即使其他客服端 client2 的数据提前到来,也不能处理 client2 客服端的请求。
  • (2) 有一个严重的问题就是,如果客户端 client1 一直没有 write 数据到来,那么服务端 service 会一直阻塞,不能处理其他客户的服务。

上面就是 Redis 通过 Unix socket 的方式来接收来自 client 端的连接存在的 I/O 阻塞问题,而 **「I/O 多路复用」**就是为了解决服务端一直阻塞等待某一个 client 的数据到来,即使其他client的数据提前到来,也不会被处理的问题。

四、Redis的单线程IO多路复用模型

为什么 Redis 中要使用 I/O 多路复用这种技术呢?因为 Redis 是跑在**「单线程」中的,所有的操作都是按照顺序线性执行的,但是「由于读写操作等待用户输入 或 输出都是阻塞的」,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务。而 I/O 多路复用就是为了解决这个问题而出现的。「为了让单线程(进程)的服务端应用同时处理多个客户端的事件,Redis 采用了 IO 多路复用机制。」**

这里**“*多路*”【指的是多个网络连接客户端】“*复用*”【指的是复用同一个线程(单进程)】**

I/O 多路复用:其实是使用一个线程来检查多个 Socket 的就绪状态,在单个线程中通过记录跟踪每一个 socket(I/O流)的状态来管理处理多个 I/O 流。

请添加图片描述

五、通信流程和总结

客户端与Redis通信的一次完整流程

1.在redis启动初始化的时候,redis会将连接应答处理器跟AE_READABLE事件关联起来,接着如果一个客户端跟redis发起连接,此时会产生一个AE_READABLE事件,然后由连接应答处理器来处理跟客户端建立连接,创建客户端对应的socket,同时将这个socket的AE_READABLE事件跟命令请求处理器关联起来。

2.当客户端向redis发起请求的时候(不管是读请求还是写请求,都一样),首先就会在socket产生一个AE_READABLE事件,然后由对应的命令请求处理器来处理。这个命令请求处理器就会从socket中读取请求相关数据,然后进行执行和处理。

3.接着redis这边准备好了给客户端的响应数据之后,就会将socket的AE_WRITABLE事件跟命令回复处理器关联起来,当客户端这边准备好读取响应数据时,就会在socket上产生一个AE_WRITABLE事件,会由对应的命令回复处理器来处理,就是将准备好的响应数据写入socket,供客户端来读取。

4.命令回复处理器写完之后,就会删除这个socket的AE_WRITABLE事件和命令回复处理器的关联关系

请添加图片描述

总结

请添加图片描述

IO线程只负责 读socket并解析请求、写socket,不负责命令的处理;IO线程要么在读,要么在写,不会同时读或写。

具体实现步骤:
1.主线程负责接收建立连接的多个socket放入全局等待队列中,等待读处理队列
2.等待队列满,将等待读队列平均分配给IO线程(多线程),进行socket与线程的绑定;
3.主线程阻塞,等待IO线程(多线程)读socket并解析请求;
4.主线程(单线程)执行所有请求命令,并将执行结果数据存入缓冲区;
5.主线程阻塞,等待IO线程(多线程)回写socket;
6.socket回写完毕,接触绑定,清空等待队列,等待客户端后续请求

也就是说具体命令执行还是由main线程所在的事件循环单线程处理,只是读写socket事件由IO线程来处理。虽然多线程方案能提升1倍以上的性能,但仍存在瓶颈:

  1. 首先所有命令的执行仍然在主线程中进行,仍然存在性能瓶颈。
  2. 另外IO 读写为批处理读写,即所有 IO线程先读取完请求数据并且解析为redis命令后,主线程才开始执行解析的命令;然后等待主线程执行完所有的redis命令后,才让所有 IO线程再一起回复所有响应;也就是说不同请求需要相互等待,效率不高。

参考链接1

参考链接2

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值