Redis的事件驱动模型
网络处理模型
- 单线程网络处理模型
- 多线程网络处理模型
- 线程池网络处理模型
- I/O多路复用网络处理模型
==注意:==网络请求皆是从网卡到达用户程序的I/O请求
Redis 事件循环
在 Redis 中,底层就是使用 IO 多路复用处理网络请求。并且创建一个 EventLoop 对象专门处理事件。
Epoll只负责I/O事件的监听,记录少量的标志性信息,当调用Epoll的用户程序得到响应后使用标记信息去程序中获取后续操作需要的信息。

上图是 EventLoop 与 Epoll 的对应关系。往 Epoll 注册指定文件描述符的事件之前,会在 EventLoop 上记录该文件描述符监听的事件和事件通知时需要执行的函数。
当 Epoll 发现 sofd 上有可读事件发生,会通知给用户线程。用户线程可以拿到 sofd 在 EventLoop 上找到相应的记录,拿到记录上的函数,执行该函数(相当于是回调操作)。
[!QUESION] redis监听一个端口,和客户端产生了很多连接,此时Redis会每个连接都生成一份文件描述符还是所有的连接都绑定在这一个文件描述符中?
Redis 监听一个端口时,操作系统底层会为这个监听的端口创建一个文件描述符(socket)。当客户端尝试连接到 Redis 服务器时,每个成功建立的连接都会由操作系统创建一个新的文件描述符来代表这个连接,用于管理和维护这个连接的状态和数据交换。这允许 Redis 服务器能够并发处理多个客户端连接,每个连接都有自己的上下文和资源管理。
Redis的事件响应流程
服务端启动
- Redis 服务启动,根据配置文件配置的端口创建套接字(Socket)
- 为这个套接字创建对应的文件描述符,使用文件描述符在Redis的EventLoop中注册对应的事件,并设置回调函数为
tcpAcceptHandler。当有事件发生时,会调用这个函数。 - 将这个文件描述符注册到事件监听器(select,poll,epoll,kqueue)中,监听连接请求事件(读事件)。

客户端连接
- 当客户端尝试连接到Redis服务器的监听端口时,操作系统会将连接请求放入监听套接字的队列中。
- 事件监听器发现监听的套接字上有读事件发生(即有新的连接请求),通知Redis服务器处理这个事件。
- Redis服务器会调用“accept()”系统调用来接受连接请求。“accept()”会从监听套接字的队列中取出一个连接请求,创建一个新的套接字,并为这个套接字分配一个新的文件描述符。
- Redis使用新的文件描述符在Redis服务的EventLoop中注册读事件,并设置回调函数为 readQueryFromClient。
- Redis将文件描述符注册到事件监听器中,监听这个连接上的读事件。

执行完这个过程后,就完成了客户端连接,并等待客户端发送请求。
客户端发送请求
客户端连接上服务器之后,接着就要发送请求,让服务器执行命令,返回数据。这个过程会完成以下几个步骤:
- 客户端发送请求,数据到达Socket缓冲区
- Epoll监听到文件描述符有可读事件产生(这个Socket对应的缓冲区中有数据),通知用户线程这个文件描述符发生了可读事件。
- 用户现成通过文件描述符从EventLoop中获取对应注册的事件,执行这个事件的回调函数(也就是readQueryFromClient函数),处理此次请求。
- readQueryFromClient函数
- 调用read系统命令读取Socket缓冲区中的数据
- 解析命令
- 执行命令
- 返回响应
[!QUESTION] 客户端通过连接发送请求,数据到达缓冲区。这个缓冲区是什么?Socket缓冲区是一块类似于内存的区域还是将这个请求数据临时保存在了文件描述符中?
当客户端通过连接发送请求,数据到达服务器时,这些数据首先被存放在操作系统内核提供的缓冲区中,这个缓冲区通常称为socket缓冲区。这个缓冲区是内核为了管理每个socket连接的输入和输出数据而设立的一块特定的内存区域。它不是物理存储在文件描述符中,而是与每个socket关联的内存区域。
[!QUESTION] 文件描述符中会保存什么数据?
文件描述符本身只是一个引用或指针,指向操作系统内部的一个数据结构,这个数据结构包含了操作系统用来管理打开的文件或socket等资源的各种信息。
[!QUESTION] redis在处理单次请求时,这个连接的事件的回调函数是否会发生改变,如果这个连接有新的请求过来会如何处理?排队吗?
这个事件回调不会发生改变,一直都是readQueryFromClient函数,然后新到的请求会存放在接受缓冲区中,当Redis服务处理完成一个请求后会将结果存放到发送缓冲区中等待发送。然后Redis服务就可以继续从接受缓冲区中获取新的请求并解析处理。
[!QUESTION] 谁负责将发送缓冲区中的数据通过连接发送给客户端?
数据写入发送缓冲区后,接下来的数据发送过程由操作系统的网络栈负责。操作系统会在网络条件允许的情况下,自动从发送缓冲区取出数据并通过网络发送给客户端。这一过程对于 Redis 来说是透明的,Redis 本身不需要直接管理数据包的发送。
Redis单线程缺点思考
==具体问题==:Redis使用事件驱动模型是因为每一个操作都很简单并且很快,那么现在我有一个问题,如果出现一个执行速度很慢的命令,比如超大批量的统计数量或者自定义的Lua函数执行的很慢,这是否会导致Redis卡顿?
由于 Redis 使用单线程模型来处理命令,如果出现执行速度很慢的命令,确实会导致 Redis 处理其他请求的延迟,进而影响整体的性能和响应速度。
原因
- 单线程模型:Redis 的核心是基于单线程的事件驱动模型。这意味着所有的命令处理、网络请求处理等都在同一个线程中顺序执行。这种设计简化了并发处理,避免了锁的开销,利用了现代CPU的高速缓存机制,通常能提供极高的性能和良好的性能稳定性。
- 长时间命令执行:当执行一个长时间的操作时(如处理超大数据集的命令、复杂的 Lua 脚本等),这个操作会占用 Redis 主线程,直到完成为止,期间 Redis 无法处理其他任何命令,导致服务响应其他请求的延迟。
解决和缓解策略
- 避免长时间操作:尽可能地避免使用会导致长时间执行的命令。例如,可以将大批量操作分解为多个小批量操作。
- 使用 Lua 脚本时注意性能:尽管 Lua 脚本提供了高度的灵活性,但应谨慎使用,避免执行复杂的数据处理操作。对于复杂逻辑,可以考虑在客户端处理。
- 读写分离:可以使用 Redis 复制功能,将读操作和写操作分离到不同的实例上,以此减轻主实例的负载。
- 使用更多的 Redis 实例:通过使用 Redis 集群或分片,可以将数据和负载分散到多个实例上,从而避免单个实例成为性能瓶颈。
- 监控和优化:定期监控 Redis 的性能指标,如命令执行时间、内存使用情况等。对出现性能问题的操作进行优化或重构。
结论
虽然 Redis 的单线程事件驱动模型在绝大多数场景下提供了优秀的性能,但确实存在因长时间执行的命令导致的卡顿问题。通过合理的设计和策略选择,可以在很大程度上避免这种情况的发生,保证 Redis 服务的高效和稳定。