Day18
1. 讲一下IO多路复用?
IO 多路复用是一种在单个线程中同时监听多个文件描述符(fd)是否就绪(可读、可写)的技术。当某个 fd 准备好后再进行实际的 IO 操作,从而避免阻塞等待。
- select:底层使用位图(bitmap)结构来表示文件描述符集合,比如一个 fd_set 可以用一个 1024 位的数组来表示 0~1023 的 fd 是否监听。每次系统调用时,用户态将整个位图拷贝到内核态,内核遍历所有 fd,通过轮询判断每个 fd 是否就绪。就绪后修改位图,并将其返回给用户。由于每次都要重新构建 fd_set 并全量拷贝,加上线性扫描O(n),在高并发场景下效率低、性能差。
- poll:底层使用数组(或者链表)结构来保存多个 pollfd 结构体,每个结构体包含一个 fd、监听事件和返回时间。调用
poll()
时,用户态数组被拷贝到内核态,内核遍历这个数组中所有 fd,检查状态是否就绪,然后将结果通过 revents 字段返回给用户。虽然 poll 支持更多 fd,不再受位图限制,但仍然是全量遍历和每次拷贝,导致性能瓶颈依旧存在。- epoll:epoll 在内核中使用两个关键的数据结构:红黑树和就绪链表。当调用
epoll_ctl()
注册 fd 时,内核将 fd 插入红黑树中管理,时间复杂度为 O(logn)。当某个 fd 上的事件发生时,内核将该 fd 加入就绪链表。调用epoll_wait()
时,直接从就绪链表返回事件(事件驱动(触发式),只处理就绪事件),避免全量扫描与频繁拷贝,效率极高。此外,内核和用户空间通过 mmap 实现共享内存结构,避免频繁内存复制。
2. epoll 边缘触发和水平触发
- 水平触发(LT):水平触发是 epoll 的默认工作模式,表示只要某个 fd 的事件条件满足(比如缓冲区有数据未读),每次调用
epoll_wait
时都会返回这个 fd。它实现简单,即使一次没有读完数据,系统仍会持续提醒你处理,但也因此可能重复触发、性能较低,适用于普通网络应用开发。- 边缘触发(ET):边缘触发是 epoll 的高效模式,仅在 fd 状态发生变化时(例如从无数据变为有数据)才触发一次事件通知;这要求程序必须使用非阻塞 IO,并在事件发生时一次性读/写干净所有数据,否则可能收不到后续通知。虽然实现复杂,但能显著减少系统调用次数,提高高并发处理能力,常用于高性能服务器如 Nginx。
3. redis怎么实现IO多路复用?
Redis 采用 I/O 多路复用技术的原因是:它本身是单线程架构,为了避免在处理客户端请求时因某个连接的阻塞 I/O 操作(如读/写)导致整个线程停顿,Redis 使用多路复用来同时监听多个连接的就绪状态,从而在一个线程内高效地管理大量并发客户端,保证系统高性能和低延迟。
Redis 的 I/O 多路复用模型基于操作系统的多路复用机制(如 epoll),每个客户端连接对应一个 socket,也就是一个文件描述符(fd)。Redis 会将所有客户端的 fd 注册到多路复用程序中,监听它们的读写事件。当客户端有数据可读或可写时,多路复用程序会触发对应的事件,通知 Redis 的文件事件处理器。文件事件处理器在单线程内依次调用各个就绪 fd 绑定的事件处理函数,执行 accept、read、write 等操作。这样,Redis 在一个线程中就能高效监控和处理大量客户端连接,避免了单个 I/O 操作阻塞导致的整个进程停顿。Redis 采用的 I/O 多路复用模式属于 Reactor 设计模式,实现了非阻塞、高并发的网络通信处理。
4. redis的网络模型是怎样的?
Redis 6.0 之前采用单 Reactor 单进程的网络模型,整个模型基于一个进程运行。Reactor 通过 I/O 多路复用技术,如 epoll,监听客户端连接与数据读写事件。当有请求到来,Reactor 直接在该进程内处理业务逻辑,像读写内存中的键值对数据结构。这种方案优势明显,代码实现简单,无进程间通信和竞争问题,且 Redis 基于内存操作,速度快,性能不受 CPU 限制。但它也存在短板,无法利用多核 CPU 性能,若业务处理耗时,如对大型集合排序,会阻塞整个进程,导致其他请求响应延迟,仅适用于业务处理快速的场景。
Redis 6.0 及之后对网络 I/O 处理进行多线程优化,但命令执行依旧单线程。主线程作为 Reactor,持续监听套接字事件,当有新连接或数据到达,会将请求分配给工作线程池。工作线程默认数量与 CPU 核数相关,可配置,它们专门负责网络数据读取和解析,通过批量读取多个客户端数据,利用多线程并行处理,减少 I/O 等待时间。解析后的命令任务传递给主线程,主线程按顺序执行具体操作,确保原子性和顺序性。这样设计主要是因为网络硬件性能提升后,Redis 性能瓶颈转移到网络 I/O,优化后能提升网络吞吐量,适应高并发场景。
5. redis哪些地方使用了多线程?
- Redis 从 6.0 版本开始对网络 I/O 处理引入多线程机制,以应对高并发场景下的性能瓶颈。在该机制下,主线程作为 Reactor,持续监听客户端连接与数据读写事件,当检测到事件发生时,不再像旧版本那样单线程处理,而是将套接字分配给预先设定的工作线程池。工作线程池默认线程数量与服务器 CPU 核数相关(通常为 CPU 核数的一半,也可通过 io-threads 参数灵活配置,最大支持 16 个线程)。这些工作线程会采用 readv 等系统调用批量读取多个客户端数据,并行解析命令参数,比如将 SET key value 拆解为对应参数数组。完成解析后,工作线程将命令任务提交给主线程,主线程再按顺序执行具体命令逻辑并返回结果。通过这种方式,Redis 在百万级并发连接场景下,网络吞吐量显著提升 30% - 50%,有效满足了秒杀、实时计数等高频小数据包交互的业务需求。
- 在数据持久化方面,Redis 支持 RDB 和 AOF 两种方式,从 6.0 版本起,其持久化操作也融入了多线程设计理念。对于 RDB 持久化,传统模式下会 fork 子进程进行全量快照生成,这个过程可能会阻塞主线程。而改进后,在数据量较大时,RDB 快照的生成过程可以通过后台线程并行处理,减少对主线程的影响;AOF 持久化主要负责将写命令追加到日志文件,在多线程模式下,AOF 的写入操作同样能在后台线程异步执行,主线程只需将写命令传递给后台线程,无需等待日志落盘即可继续处理后续请求,避免了因持久化操作导致的响应延迟,极大提升了 Redis 在数据持久化过程中的整体性能与响应速度。
- Redis 还会利用多线程卸载一些耗时任务,以此保障主线程的高效运行。在 Redis 集群模式中,节点间的通信、故障转移检测等操作通常较为耗时,若由主线程处理会阻塞其他请求。因此,这些任务会被分配给后台线程执行,使其在不影响主线程处理客户端请求的情况下,完成集群状态维护;另外,UNLINK 命令用于删除键值对时,若数据量庞大,删除操作可能会占用较长时间,此时 Redis 会将该删除任务交由后台线程异步处理,主线程接收命令后立即返回,告知客户端删除操作已在处理中,从而避免了因长时间删除操作导致的请求阻塞,降低客户端请求延迟,让主线程能迅速响应其他新的客户端请求。
- Redis 所采用的 jemalloc 内存分配器也与多线程机制紧密配合。jemalloc 可配置一个后台线程专门负责内存释放与整理工作。当 Redis 释放内存时,若采用单线程操作,在释放大量内存块时可能会产生阻塞,影响系统性能。而借助后台线程,内存释放操作可以在后台异步进行,与主线程的内存分配操作并行,这不仅提高了内存管理效率,还减少了内存碎片化问题,使得 Redis 在频繁进行内存分配与释放的场景下,依然能够保持高效稳定的运行状态 。