LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

Redis凭什么用单线程“干翻”了全世界?

admin
2025年8月24日 21:33 本文热度 62

Redis (Remote Dictionary Server),即远程字典服务,是一个开源的、使用 C 语言编写的、高性能的内存键值 (Key-Value) 数据库。


在工程实践中,想必你也或多或少都接触过它,但是,Redis作为“单线程”应用,为什么它会这么快?你可能会说因为用了内存,其实这只是其中一点,下边让我们一起揭晓答案吧。


这里要特别指出的是,Redis的“单线程”,主要是指网络IO和实际的key-value读写是由一个线程完成的,但是,持久化、数据删除、AOF重写等,其实还是由额外的线程执行的。

并且,从Redis 6.0开始,网络IO也采用多线程,只有读写命令是用单线程处理。


01

“3个因素”造就了Redis的高性能


Redis以其出色的性能和灵活性,常被称为数据处理领域的“瑞士军刀”,常用于缓存、会话存储、消息队列、分布式锁等场景。

Redis高性能的根因可以归纳为以下3点:


(1)

内存为王-物理定律上的优势

这是 Redis 高性能的基石,Redis数据存放在内存中,是它与其他基于磁盘的数据库(如 MySQL, PostgreSQL, MongoDB)的重要区别之一。

(2)

极致的数据结构

Redis内部对哈希表、跳表等数据结构做了特别优化,会根据数据的大小和类型,动态选择最优的内部编码,实现“空间换时间”或“时间换空间”,极大地减小了网络开销和客户端的压力。

(3)

高效的IO模型

采用了IO多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。


前两点的物理内存的优势和优化的数据结构,比较好理解,今天我们重点说的是Redis的IO多路复用模型。


02

为什么要用IO多路复用?


理解IO模型前,我们来看一下,IO多路复用到底能解决什么问题?


一句话概括:

IO多路复用(I/O Multiplexing) 是一种机制,它允许一个单独的进程(或线程)监视多个文件描述符(File Descriptor, FD),一旦其中任何一个FD准备好进行IO操作(比如可读、可写或出现异常),该机制就会通知相应的进程。


为了更好地理解,我们用一个经典的场景举例:

一个网络服务器需要同时处理成百上千个客户端连接,会面临的3种选择:


1、阻塞IO + 每次连接一个新的进程/线程模型


工作方式

每当一个新客户端连接进来,服务器就创建一个新的进程或线程专门为它服务。这个线程在等待客户端数据时,会调用read()或recv(),然后阻塞(block),直到数据到达。


缺点:

  • 资源消耗巨大: 

    每个进程/线程都需要消耗内存(如栈空间)和CPU资源。成千上万个连接就意味着成千上万个线程,系统根本无法承受。

  • 上下文切换开销大: 

    CPU需要在这些大量的线程之间频繁切换,这本身就是一笔巨大的开销,导致实际用于处理业务逻辑的时间大大减少。


2、非阻塞IO + 忙轮询(Busy-Polling)


工作方式: 

将每个连接的IO操作设置为非阻塞模式。然后用一个循环,不断地去轮询(“询问”)每一个连接:“你有数据要读吗?” “你可以写数据了吗?”


缺点:

  • CPU空转: 

    即使大部分连接都没有事件发生,循环也会一直运行,不停地做无用功,导致CPU使用率100%,造成巨大的浪费。


3、IO多路复用


工作方式:

  • 应用进程将一批需要监视的文件描述符(代表所有客户端连接)“注册”给内核;

  • 应用进程调用一个阻塞函数(如select(), poll(), epoll_wait()),然后自己就去“睡觉”了,不占用CPU;

  • 内核开始作为“代理”或“管家”,在底层持续监视这些FD。当任何一个或多个FD准备就绪(例如,某个客户端发来了数据),内核就会唤醒正在“睡觉”的应用进程;

  • 应用进程被唤醒后,返回的结果会明确告诉它哪些FD已经准备好了;

  • 应用进程只需要处理那些真正准备就绪的连接,进行读写操作。


优势:

IO多路复用就是为了解决以上两个模型的痛点而生的。

它引入了一个“代理”或“协调者”(即内核中的select, poll, epoll等机制)。

这样,一个线程就能高效地管理海量的连接,既避免了多线程的资源和切换开销,也避免了忙轮询的CPU浪费。


03

Linux 3种IO多路复用实现方式


Linux系统为我们提供了3种主要的IO多复用API,即经常能看到的select、poll、epoll


1、select


select 是最早的、最经典的多路复用实现,遵循POSIX标准,因此可移植性最好。


工作原理:

  • 创建一个fd_set(一个位图结构),把要监视的FD对应位置为1;

  • 调用select( )函数,将这个fd_set从用户空间拷贝到内核空间

  • 内核遍历所有被监视的FD,检查它们的状态;

  • select( )返回后,内核会将准备就绪的FD对应位置为1,未就绪的置为0,再将修改后的fd_set拷贝回用户空间

  • 用户进程需要再次遍历整个fd_set,找出哪些FD是准备就绪的。

缺点:

  • 文件描述符数量限制: 

    fd_set的大小是固定的(通常是1024),限制了能监视的FD数量。

  • 重复拷贝开销: 

    每次调用select都需要在用户空间和内核空间之间来回拷贝fd_set,连接数越多,开销越大。

  • 线性扫描开销:

    内核和用户进程都需要遍历整个FD集合来查找就绪的FD,效率是O(n),其中n是监视的FD总数。即使只有一个FD就绪,也得全部扫一遍。


2、poll


poll 是对select的改进,解决了FD数量限制的问题。

工作原理:

  • 它使用一个pollfd结构体数组来代替fd_set。这个数组没有固定大小限制,可以动态分配。

  • 工作流程与select类似,仍然需要将整个pollfd数组从用户空间拷贝到内核空间,并且返回后需要遍历数组来查找就绪的FD

优点:

  • 没有FD数量限制,只受限于系统资源。

缺点:

  • 仍然存在重复拷贝和线性扫描的开销,性能问题在连接数巨大时依然存在。


3、epoll(Event Poll)


epoll 是Linux下对select和poll的重大改进,是目前公认的在Linux上实现高性能网络服务器的首选。Nginx、Redis等都使用了它。


工作原理: 

epoll 的设计思想完全不同,它引入了三个核心函数:

  • epoll_create( )

    在内核中创建一个epoll实例(可以想象成一个事件中心),返回一个代表该实例的FD。这个实例内部包含一个红黑树(用于快速查找FD)和一个就绪链表(用于存放已就绪的FD)。

  • epoll_ctl( )

    用于向epoll实例中添加、修改或删除要监视的FD。当注册一个FD时,内核会将这个FD和一个回调函数关联起来。当该FD就绪时,内核会自动调用这个回调函数,将其放入就绪链表中。

  • epoll_wait( )

    阻塞等待,直到epoll实例的就绪链表非空。一旦有就绪的FD,epoll_wait就会被唤醒,并直接返回就绪FD的列表。

优点:

  • 没有FD数量限制。

  • 避免重复拷贝:

    epoll_ctl将FD注册到内核后,就不需要每次调用epoll_wait时都重复拷贝FD列表了。它基于事件驱动,由内核来记录和跟踪FD状态。

  • 效率极高:

    epoll_wait返回时,直接返回的就是就绪的FD列表,用户进程无需再遍历整个集合。其时间复杂度是O(1),与监视的FD总数无关,只与活跃的FD数量有关。



04

Redis的IO模型


1、Redis网络IO处理中的核心步骤


以一个GET请求为例,Redis 网络IO处理大概有如下过程:

网络IO处理流程


  • bind() 

    将一个创建好的套接字(Socket)与一个具体的 IP 地址和端口号关联起来。

  • listen()

    当套接字绑定好地址和端口后,listen() 函数会让这个套接字进入“被动监听”状态,准备开始接收来自客户端的连接请求。它还会定义一个“待办队列”(backlog)的大小,用来存放那些已经完成 TCP 三次握手但服务器还未来得及处理(accept)的连接。

  • accept()

    默认情况下是一个阻塞函数。当一个客户端成功连接到服务器的监听端口后,accept() 函数会从 listen 的“已完成连接队列”中取出一个连接,并为这个连接创建一个新的套接字(已连接套接字)。后续服务器与这个特定客户端的所有通信,都将通过这个新的套接字进行,而原来的监听套接字则继续负责监听新的连接请求。

  • recv()

    当连接建立后,服务器使用 recv()函数从 accept() 创建的已连接套接字中读取客户端发送过来的数据。数据会被读入到一个应用程序的缓冲区(buffer)中,以便后续处理。这也是数据从内核空间进入用户空间的环节。

  • parse()

    parse 不是一个标准的网络 IO 函数,而是一个应用层逻辑的统称。从 recv() 接收到的是原始的字节流(byte stream)。应用程序需要根据预先定义好的协议来解析这些字节流,理解其含义。比如,对于一个 HTTP 请求,解析就是要识别出请求行、请求头和请求体,弄清楚客户端想要做什么。

  • get()

    get() 函数从数据库中读取值。

  • send()

    当服务器处理完请求并准备好响应数据后,会调用 send()函数,通过已连接套接字将响应数据发送回客户端。这是数据从用户空间进入内核空间,并由协议栈负责发送出去的环节。


2、非阻塞模式设置


上述函数中,

  • bind()

    绑定地址和端口,是本地操作,几乎瞬间完成。

  • listen()

    将套接字转换为监听状态,也是一个本地内核操作,立即返回。

  • parse()和get() 都偏向于应用层逻辑。

但在默认情况下,accept()、recv()、send()是阻塞模式的,也就是说,当 Redis 监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接。类似的,当 Redis 通过 recv()/send() 从一个客户端读取或返回数据时,如果数据一直没有到达或返回,Redis 也会一直阻塞在 recv()/send()。


不过,幸运的是,socket 网络模型本身支持非阻塞模式。


在网络 I/O 处理中,将套接字(Socket)设置为非阻塞模式,主要影响的是那些需要“等待”网络事件的函数。

一个套接字一旦通过 fcntl() 或 ioctl() 设置为非阻塞模式,所有作用于该套接字上的 I/O 函数都会变成非阻塞的。


(1)accept() - 接受连接


  • 阻塞模式(默认):
    如果服务器的全连接队列(Accept Queue)中没有已经完成三次握手的连接,accept() 调用会阻塞,程序会一直停在这里,直到有新的客户端连接到来。

  • 非阻塞模式效果:
    如果全连接队列中没有可用的连接,accept() 不会阻塞,而是会立即返回一个错误码 EAGAIN 或 EWOULDBLOCK。这告诉应用程序:“现在没有新连接,你可以先去做点别的事,稍后再来试试。”

  • 核心用途:
    这是实现高性能网络服务器(如 Nginx、Redis)的基石。服务器主线程可以将监听套接字注册到 I/O 多路复用机制(如 epoll)上。当 epoll 通知有新连接事件时,才去调用 accept(),此时可以确保 accept() 不会阻塞,并且通常在一个循环里调用它,直到取完所有待处理的连接。


(2)recv() - 接收数据


  • 阻塞模式(默认):
    如果套接字的内核接收缓冲区中没有数据,recv() 调用会阻塞,直到有数据到达。如果对端一直不发数据,程序就会一直被挂起。

  • 非阻塞模式效果:
    如果接收缓冲区中没有数据,recv() 不会阻塞,而是会立即返回一个错误码 EAGAIN 或 EWOULDBLOCK。这表示“现在没数据可读”。如果缓冲区有数据,它会读取尽可能多的数据并返回读取的字节数。

  • 核心用途:
    在事件驱动模型(Event Loop)中,程序只在 epoll 通知套接字“可读”时才去调用 recv(),从而保证了 recv() 不会阻塞,使得单线程可以高效处理大量连接的数据读取,而不会因为等待某个慢客户端而卡住。


(3)send() - 发送数据


  • 阻塞模式(默认):
    如果套接字的内核发送缓冲区已满(通常是因为对端接收速度慢,导致数据积压),send() 调用会阻塞,直到缓冲区有足够的空间来容纳要发送的数据。

  • 非阻塞模式效果:
    如果发送缓冲区已满,send() 不会阻塞。它会立即返回错误码 EAGAIN 或 EWOULDBLOCK。如果缓冲区还有部分空间,它会尝试写入尽可能多的数据,并返回实际写入的字节数。

  • 核心用途:
    防止因为网络拥塞或慢速的接收端导致整个发送程序阻塞。同样,在事件驱动模型中,程序会先把要发送的数据放在应用层缓冲区,然后等待 epoll 通知套接字“可写”(即发送缓冲区有空间了)时,再调用 send() 将数据写入。



3、Redis实现

Redis的epoll机制


毫无疑问,Redis采用select/epoll模式来提升进行IO操作。

上图中的多个 FD 就是多个套接字。Redis 网络框架调用 epoll 机制,让内核监听这些套接字。此时,Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。

为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。

那么,回调机制是怎么工作的呢?

其实,select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。

这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。

这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。


05

小结


Redis之所以是性能王者,来自于使用了“物理优势”的内存、极致的数据结构和高效的IO模型。


IO模型的高效,是因为它采用了内核的epoll机制,使得Redis无需“关注”IO等待,可以持续不断地对事件队列进行处理,所以能及时迅速地响应客户端请求,达到性能最优。



阅读原文:原文链接


该文章在 2025/8/25 13:07:18 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved