Redis 事件驱动与IO多线程

218 阅读6分钟

「这是我参与 2022 首次更文挑战的第 11 天,活动详情查看:2022 首次更文挑战

结庐在人境,而无车马喧。

前言

Redis 是在开发中非常好用的缓存中间件,在 Redis 的发展历史上有几个重要的变动:

  • 1 Redis2.6 2012.10 开始支持 lua 脚本。
  • 2 Redis3.0 2015.4 发布 redis 集群方案。
  • 3 Redis4.0 2017.7 支持混合持久化(rdb 和 aof)以及多线程异步删除(unlink)。
  • 4 Redis6.0 2020.5 支持 io 多线程。

在我们的印象中, Redis 单线程的,也能抗住大多数的业务场景。Redis 单线程也快的飞起的原有 : 1 数据结构简单,而且是完全基于内存的操作,空间复杂的和时间复杂的都很低 2 核心基于非阻塞的 IO 多路复用机制,3 单线程操作避免了多线程的上下文切换带来的额外性能开销

Redis 单线程既然都能解决问题,那么为什么还要支持多线程呢?

Redis 的架构

Redis 是一个经典模式的 Reactor 网络模型,对于一个请求,其完整的处理过程如下所述:

graph LR
A[读取Socket] -->B(解析请求)  -->C(执行操作)  -->D(写回Socket)

RedisC 语言开发的,基于 Reactor 模型构建了 网络事件处理器、文件事件处理器。Redis 是单线程的,为了提高 IO 性能,使用 IO 多路复用器(有 select、poll、epoll 三种,redis 使用了 epoll)来同时监听多个 Socket,根据客户端请求的 Socket 上的事件类型来选择对应的事件处理器来处理这个事件。这样做即可以实现高性能的网络通信模型,又可以跟内部数据处理的单线程模块进行对接,保证了 Redis 内部的线程模型的可靠和稳定性。

Redis 是一个事件驱动的程序,主要处理两类事件: 文件事件和时间事件,两者之间不冲突,是合作关系服务器会轮流处理两类事件。

  • 文件事件: 文件事件是对 Socket 操作的抽象,服务器和客户端之间通信产生文件事件,服务器通过监听这一事件来完成一系列的业务操作。

  • 时间事件: Redis 定时处理一些定时任务,比如数据的存储 RDB、AOF、定时删除键都是Redis 定期去执行的,Redis 一般通过 serverCron执行时间事件。

由于 Redis 是单线程的,虽然避免了线程切换上下文的开销,但同时也造成了对 CPU 核心数利用率不足。虽然 Redis4.0 引入了 多线程来解决数据异步删除的功能,但是其核心 数据读写和数据处理还是一个线程,所以仍然是单线程的。

影响 Redis 的性能主要有:CPU 、内存 和网络 IO。内存大小跟机器的设置和 Redis 的配置有关,单线程的 Rediscpu 的利用率都不高,那么影响 Redis 的性能主要就是网络 IO 这一块儿了, 先看一下 Redis 的基本结构图:

Redis 的事件处理主要由文件事件处理器来完成的,其结构包含:服务端 Socket、IO 多路复用器、文件事件分派器和事件处理器(连接应答处理器等、命令请求处理器、命令回复处理器)。多个 Socket 可能并发的产生不同的事件,IO 多路复用器会监听多个 Socket,会将 Socket 放入一个队列中排队,每次从队列中有序、同步取出一个 Socket 给事件分派器,事件分派器把 Socket 给对应的事件处理器。

然后一个 Socket 的事件处理完之后,IO 多路复用器才会将队列中的下一个 Socket 给事件分派器。文件事件分派器会根据每个 Socket 当前产生的事件,来选择对应的事件处理器来处理。一般的顺序就是建立连接,处理请求、命令回复等三个步骤。

  • 1、当Redis 启动后,器多路复用器会监听 AE_READABLE 事件,该事件与连接应答处理器关联,可以理解为 SocketServet.accpet

  • 2、当客户端发起连接,就会发生一个 AE_READABLE 事件,然后连接应答处理器负责和客户端建立连接,创建客户端对应的 socket,同时将这个 socketAE_READABLE 事件和命令请求处理器关联,这样客户端就可以向 Redis 服务器发送命令请求,可以类比为 Socket s = ss.accept

  • 3、当客户端向 Redis 发请求时(不管读还是写请求,比如 set user:age 12 或者 get user:age),客户端 socket 都会产生一个 AE_READABLE 事件,触发命令请求处理器。处理器读取客户端的命令内容,然后传给相关程序执行。

  • 4、当 Redis 服务器准备好给客户端的响应数据后,会将 socketAE_WRITABLE 事件和命令回复处理器关联,当客户端准备好读取响应数据时,会在 socket 产生一个 AE_WRITABLE 事件,由对应命令回复处理器处理,即将准备好的响应数据写入 socket 供客户端读取。

  • 5、命令回复处理器全部写完到 socket 后,就会删除该 socketAE_WRITABLE 事件和命令回复处理器的映射。

Redis 的 IO 多线程

Socket 的读取和写回操作时比较耗时的,解析请求和内存操作耗时远小于 IO 操作。一般情况下,我们能想到的多线程改造,就是将客户端的请求放进队列里面,然后搞一个线程池从队列里获取请求进行数据处理和请求返回,但是这样的势必会带来线程安全的问题,一旦使用到锁,就会带来额外的性能开销,还会影响其底层的数据结构。Redis 的多线程改造,只是将比较耗时的 IO 操作进行多线程操作,如上图所示的红色虚线括起来的内容进行多线程处理,核心的数据操作还是单线程处理,这样即提高了数据读写和解析协议的效率,也保障了执行数据操作命令的安全性,可谓是别出心裁,另辟蹊径。

总结

本文讲述了 Redis 单线程为何还可以如此快,也解释了 Redis 多线程的改造点。阐释了 IO 多路复用和事件处理机制Redis 的核心数据处理和操作还是单线程的。