2023-08-12
原文作者:Ressmix 原文地址:https://www.tpvlog.com/article/119

在各类分布式框架中,还有一类是和高性能息息相关的,那就是分布式缓存。目前生产环境常用的开源分布式缓存框架就是Redis和Memcached,它们之间的主要区别,我这里简单比较下:

  • Redis拥有更多的数据结构,支持更丰富的数据操作。
  • Memcached没有原生的集群模式,而Redis原生支持cluster模式。

目前Redis在工业环境中用得比较多,所以本系列只介绍Redis。如果读者对Memcached感兴趣,可以参考其官方文档

一、Redis线程模型

Redis是典型的单线程架构,所有的读写操作都是在服务端的一条主线程中完成的。Redis客户端与服务端的模型如下图,每次客户端调用都经历了 发送命令执行命令返回结果 三个过程:

202308122219511101.png

既然是单线程,为什么Redis能实现这么高的读写性能呢?

因为Redis基于 Reactor 模式开发了自己的网络事件处理器—— 文件事件处理器(file
event handler)

文件事件处理器,是单线程模式运行的,但是使用了I/O多路复用技术,同时监听多个套接字,并为套接字关联不同的事件处理器,当套接字上产生文件事件时,就会调用相应的事件处理器进行处理,这样就可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了Redis内部线程模型的简单性。

文件事件处理器一共包含四个部分,它们分别是 套接字(Socket)I/O多路复用程序文件事件分派器 以及 事件处理器 ,如下图:

202308122219517272.png

可以看到,I/O多路复用程序负责监听多个socket,并向文件事件派发器传递那些产生了事件的socket。

尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生的socket都放到同一个队列里边,然后文件事件处理器会以有序、同步的方式处理该队列中的每个socket:根据每个socket当前产生的事件,来选择对应的事件处理器来处理。

1.1 套接字(socket)

Redis服务端通过Socket与客户端连接。每当一个套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,就会产生一个文件事件。 因为一个服务器通常会连接多个套接字, 所以多个文件事件有可能会并发地出现。

比如,当socket变得可读时,socket就会产生一个AE_READABLE事件;当socket变得可写的时候,socket会产生一个AE_WRITABLE事件。

1.2 I/O 多路复用程序

I/O 多路复用程序主要负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。

I/O多路复用程序会将所有产生事件的套接字都入队到一个队列里, 然后通过这个队列,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字。 当上一个套接字产生的事件被处理完毕之后, I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。

202308122219531533.png

Redis 使用的IO多路复用技术主要有:selectepollevportkqueue等。每个IO多路复用函数库在 Redis 源码中都对应一个单独的文件,比如ae_select.c,ae_epoll.c, ae_kqueue.c等。Redis 会根据不同的操作系统,按照不同的优先级选择多路复用技术。事件响应框架一般都采用该架构,比如 netty 和 libevent:

202308122219538504.png

1.3 文件事件分派器

文件事件分派器,负责监听多个套接字,接收 IO 多路复用程序传来的套接字,并根据套接字产生的事件类型, 调用相应的事件处理器,比如:
如果是客户端要连接redis,那么会为socket关联连接应答处理器;
如果是客户端要写数据到redis,那么会为socket关联命令请求处理器;
如果是客户端要从redis读数据,那么会为socket关联命令回复处理器。

1.4 事件处理器

服务器会为执行不同任务的套接字关联不同的事件处理器,这些处理器是一个个函数, 它们定义了某个事件发生时, 服务器应该执行的动作。

二、示例

我们以一次Redis客户端的请求,来看下上述的整个流程:

202308122219546245.png

  1. Redis服务启动初始化的时候,Redis会将【连接应答处理器】跟AE_READABLE事件关联起来;
  2. 如果一个Redis客户端发起连接请求,此时会产生一个AE_READABLE事件,然后由【连接应答处理器】来处理跟客户端建立连接,创建客户端对应的socket,同时将这个socket的AE_READABLE事件跟【命令请求处理器】关联起来;
  3. 当客户端向Redis发起命令请求时,首先就会在socket产生一个AE_READABLE事件,然后由对应的【命令请求处理器】来处理。这个【命令请求处理器】就会从socket中读取请求相关数据,然后进行执行和处理;
  4. 当Redis准备好了给客户端的响应数据后,就会将socket的AE_WRITABLE事件跟【命令回复处理器】关联起来,当客户端准备好读取响应数据时,就会在socket上产生一个AE_WRITABLE事件,对应的【命令回复处理器】会将准备好的响应数据写入socket,供客户端来读取;
  5. 【命令回复处理器】写完之后,就会删除这个socket的AE_WRITABLE事件和【命令回复处理器】的关联关系。

三、总结

Redis属于单线程模型,但是效率非常高,每秒能够读写上万条数据,其主要原因如下:

  1. 纯内存操作;
  2. 核心是基于非阻塞的IO多路复用机制;
  3. 单线程反而避免了多线程的频繁上下文切换问题。
阅读全文