【Redis】网络高并发模型

作者:步十人日期:2026/6/1

目录

    • 一、 核心场景:百万并发下的秒杀大考
      • 1. 传统多线程服务器会怎么样?
        • 2. Redis 凭什么能抗住?
          • 第一步:建立连接(非阻塞 + epoll)
            * 第二步:读取请求(非阻塞 I/O)
            * 第三步:执行命令(单线程串行,纯内存操作)
            * 第四步:返回结果(非阻塞写)
    • 二、 深度对比:多线程阻塞 vs 单线程非阻塞
    • 三、 演进:Redis 6.0+ 的多线程 I/O 革命
    • 四、 微观视角:一个秒杀请求的时间线拆解
    • 五、 致命死穴:如果某个命令很慢怎么办?
    • 本篇总结

一、 核心场景:百万并发下的秒杀大考

假设运营着一个电商平台,双十一零点有 100 万用户同时抢购 1 件商品。后端使用 Redis 存储商品库存:

1SET stock 1
2
3

每个用户发起请求时,需要原子地检查并扣减库存,核心业务伪代码如下:

1if stock > 0:
2    stock = stock - 1
3    return success
4else:
5    return fail
6
7

⚠️ 注意:在真实生产环境中,该操作必须使用 Redis 的 DECR 命令或 Lua 脚本 来执行,以保证复合操作的原子性。

1. 传统多线程服务器会怎么样?

假设采用传统的 Tomcat + MySQL 架构:

  • 每一个请求到达服务器后都会分配一个独立的线程。
  • 100 万并发会导致系统频繁进行线程上下文切换,引发剧烈的锁竞争,进而导致 CPU 瞬间爆满。
  • 随后,数据库的磁盘 I/O 成为致命瓶颈,整个系统几乎会立刻崩溃,或者响应时间从 1ms 飙升至 10s 以上。

2. Redis 凭什么能抗住?

面对同样的百万级并发洪峰,Redis 依靠其独特的网络模型,将整个处理链路巧妙地拆解为以下四个步骤:

第一步:建立连接(非阻塞 + epoll)
  • 注册监听:Redis 启动时,主线程会创建一个监听 Socket,并将其注册到 Linux 内核的 epoll 实例中。
  • 内核排队:当 100 万个客户端同时发起 TCP 连接时,操作系统内核会将这些连接放入全连接队列中。
  • 按需感知:Redis 主线程循环调用 epoll_wait,内核会直接返回当前有事件就绪的 Socket 列表(可能只有几千个)。
  • 高效分发:主线程依次调用 accept 接受连接,生成对应的 Client Socket,并同样注册到 epoll 中。
  • 关键点:这一步完全由 Linux 内核处理,Redis 主线程只是轮询就绪列表,而不需要盲目遍历 100 万个空闲连接,算法复杂度为 O ( 就绪数 ) O(就绪数)O(就绪数)。
第二步:读取请求(非阻塞 I/O)

每个 Client Socket 上都可能传来用户的“扣库存”命令。

  • 当数据到达时,epoll 会将该 Socket 标记为可读状态。
  • Redis 主线程从 epoll 就绪列表中取出该 Socket,调用 read 读取数据。由于采用了非阻塞模式,如果当前没有数据会立马返回,绝不原地死等。
  • 读取到的原始命令会被放入 Redis 的输入缓冲区
  • 现象:即便 100 万个连接同时发送请求,内核也会把数据暂存在 Socket 接收缓冲区中。Redis 主线程一次循环只处理几千个就绪的 Socket 并读走数据,单线程运行,没有任何线程切换开销
第三步:执行命令(单线程串行,纯内存操作)
  • Redis 从输入缓冲区中取出解析好的命令(如 DECR stock)。
  • 直接在内存中执行该修改操作(耗时通常仅需几十纳秒)。
  • 关键点:由于执行核心是单线程,同一时刻有且仅有一个命令在修改内存,因此不需要对 stock 加任何锁(天然串行化)。即便 100 万个命令同时到达,它们也会在内存缓冲区里排队依次执行,天生免疫超卖问题
第四步:返回结果(非阻塞写)
  • 命令执行完毕后,执行结果会被写入 Client Socket 的输出缓冲区
  • 如果客户端接收较慢导致发送缓冲区满了,Redis 会将该 Socket 标记为可写并注册到 epoll 中。
  • 当 Socket 恢复可写状态时,Redis 再将剩余数据异步写回客户端。整个过程不会因为某个客户端的“网速慢”而阻塞其他客户端。

通过一张图可能会看的更加清晰:

1┌─────────────────────────────────────────────────────────────────────────────────────────────────────────┐
2                                        Linux 操作系统内核 (Kernel)                                       
3 └───────────────────────────────────────┬─────────────────────────▲───────────────────────────────────────┘
4                                           100万并发连接             异步网络写 (非阻塞)
5                                            注册并触发呼叫铃           网卡空闲时推回结果
6                                                                  
7 ┌─────────────────────────────────────────────────────────────────┴───────────────────────────────────────┐
8                                      I/O 多路复用监听器 (epoll_wait)                                      
9 └───────────────────────────────────────┬─────────────────────────────────────────────────────────────────┘
10                                           返回当前活跃的【就绪队列】(如3000个)
11                                         
12 ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────┐
13                                         Redis 主线程 (Single Thread)                                     
14                                                                                                          
15   Step 1: 网络读取 (Read) ──►  Step 2: 命令解析 ──►  Step 3: 单线程核心执行 ──►  Step 4: 网络写入 (Write)   
16   ┌─────────────────────┐    ┌───────────────┐     ┌──────────────────────┐    ┌─────────────────────┐   
17    非阻塞网络读               将字节流解析          💡核心操作: 纯内存           执行结果放入           
18    从内核缓冲区把              为具体的 Redis       无论是【读内存】(GET)        输出缓冲区             
19    字节流命令搬运过来          命令                 还是【改内存】(SET)                                
20   └──────────┬──────────┘    └───────┬───────┘     └──────────▲───────────┘    └──────────┬──────────┘   
21 └─────────────┼───────────────────────┼────────────────────────┼──────────────────────────┼───────────────┘
22                                                                                        
23                                                                                        
24 ┌─────────────────────────────────────────────┐       ┌────────┴─────────────┐   ┌────────────────────────┐
25             Redis 输入缓冲区 (Input Buffer)              Redis 内存数据库         Redis 输出缓冲区       
26    [ GET name ]   [ DECR stock ]   [ ... ]           (String/Hash/List...)    (Output Buffer)        
27 └─────────────────────────────────────────────┘       └──────────────────────┘   └────────────────────────┘
28

二、 深度对比:多线程阻塞 vs 单线程非阻塞

为了更直观地理解,我们将传统多线程阻塞模型Redis 单线程非阻塞模型在处理高并发秒杀场景时的表现进行横向对比:

维度多线程阻塞模型(如传统 Tomcat)单线程非阻塞模型(Redis 自身)
并发连接能力1 个连接 → \rightarrow→ 1 个线程,1 万个线程将产生约 1GB 的内存开销1 万个连接通过 epoll 集中管理,仅需消耗 几 MB 内存
上下文切换极其频繁,导致大量 CPU 资源空转损耗 上下文切换
锁竞争开销需要依赖分布式锁或数据库事务,开销极大无锁 设计,内存直接操作
吞吐量极限受限于线程极限与锁屏障,约 5~10 万 QPS(实际往往更低)单核即可轻松跑满 10 万+ QPS
高并发稳定性洪峰下极易引发线程池耗尽,导致系统雪崩表现极其稳定,但易受慢查询命令的影响

三、 演进:Redis 6.0+ 的多线程 I/O 革命

既然单线程这么好,为什么 Redis 6.0 之后又引入了多线程?

在极端的超高吞吐场景下,网络数据的读取、解析以及结果写回(即网络 I/O) 往往会先于内存执行挤爆单核 CPU 的带宽。为了突破这个瓶颈,Redis 6.0 引入了 多线程 I/O

在秒杀场景中,它的工作模式演变为:

  1. 主线程:依然雷打不动地独占命令执行权(负责扣库存)。
  2. I/O 线程池:分配多个辅助线程,并行读取 100 万个连接的网络请求数据,并将处理好的响应结果并行写回网卡。
  3. 核心本质命令执行依然是单线程的。因此,库存扣减的原子性、无锁特性不受任何影响,只是网络传输的“大门”被拓宽了。

四、 微观视角:一个秒杀请求的时间线拆解

为了更细腻地感受 Redis 的速度,我们以微秒( μ s \mu sμs)为单位,拆解一个客户端发送 DECR stock 到接收响应的微观全过程:

1 ┌─────────────────────────────────────────────────────────────────────────────┐
2   网络传输  -->  epoll通知  -->  读数据  -->  命令解析  -->  内存执行  -->  写响应  
3    50 μs          μs         10 μs        0.5 μs       纳秒级       纳秒级   
4 └─────────────────────────────────────────────────────────────────────────────┘
5
6
  1. 网络传输:命令数据包从客户端出发,通过网络到达 Redis 服务器的内核缓冲区(约耗时 50 μ s 50\,\mu s50μs,受物理网卡影响)。
  2. epoll 通知就绪:Redis 主线程在 epoll_wait 阻塞处被唤醒,获取到可读的 Socket 列表(耗时 几 μ s \mu sμs)。
  3. 读取数据:调用 read 函数将内核数据拷贝到 Redis 的输入缓冲区(约耗时 10 μ s 10\,\mu s10μs,非阻塞,立即返回)。
  4. 命令解析:协议识别为 DECR,并在内存 Dict 中找到 Key 对应的指针(哈希查找,约耗时 0.5 μ s 0.5\,\mu s0.5μs)。
  5. 内存执行:在内存中将整数 1 扣减为 0纳秒级,极其恐怖的速度)。
  6. 写回响应:将结果 ":0\r\n" 写入输出缓冲区,并向 epoll 注册写事件(纳秒级)。
  7. 数据发送:异步将数据推回给客户端网卡(约耗时 50 μ s 50\,\mu s50μs)。

💡 小结:单个请求的闭环处理大约需要 120 μ s 120\,\mu s120μs。在实际运行中,Redis 会通过批量处理(Pipeline 等技术)进一步优化,使单核吞吐量轻松突破 10 万+ QPS


五、 致命死穴:如果某个命令很慢怎么办?

单线程模型虽然无敌,但它有一个致命的阿喀琉斯之踵

假设某个开发人员在生产环境中错误地执行了类似 KEYS * 的命令,去全局扫描一个包含 1000 万个 Key 的数据库,该命令耗时将长达 2 秒

  • 🚨 灾难发生:因为 Redis 是单线程串行执行命令的,在这 2 秒期间,主线程被完全卡死。
  • 连锁反应:在这 2 秒内涌入的所有其他命令(包括高并发秒杀扣库存的请求)全部被迫排队死等,进而导致前端大面积超时崩溃。

🛑 生产环境高压线:在企业级开发中,必须严格杜绝一切慢查询。严禁使用 KEYS *,应改用渐进式遍历命令 SCAN;同时需要合理设计大 Key,避免使用过于复杂的数据结构。


本篇总结

  1. “单线程”的本质:指 Redis 的命令执行是单线程的,这带来了天然的原子性与免锁特性。
  2. 高并发的底层依赖:高并发依靠的是 Linux 内核的 epoll(I/O 多路复用) + 非阻塞 I/O,单线程即可轻松驾驭数十万个活跃连接。
  3. 性能瓶颈的真相:在纯缓存场景中,CPU 执行命令(内存修改)极快,根本不是瓶颈;真正的瓶颈在于网络带宽以及多线程带来的上下文切换损耗
  4. 现代化演进:Redis 6.0+ 的多线程 I/O 仅仅优化了“网卡到缓冲区”的搬运速度,其灵魂(核心执行线程)依然纯粹单线程。

【Redis】网络高并发模型》 是转载文章,点击查看原文


相关推荐


你写的代码没有测试,就像出门不锁门——Jest + Testing Library 从入门到不慌
kyriewen2026/5/11

你改了一行代码,手动点了一遍页面,觉得没问题就上线了。结果用户反馈“登录按钮点不动了”。你心里咯噔:我根本没改登录相关代码啊。今天我们来给你的代码装一把“智能门锁”——单元测试。用 Jest + Testing Library,把常见 Bug 锁在门外,让你改代码时不再心惊胆战。 前言 很多前端对测试的态度是:项目那么赶,哪有时间写测试?结果修 Bug 的时间比写代码还多。你花 20 分钟写的测试,可能帮你省掉 2 小时的通宵排查。 测试不是“额外工作”,而是安全网。当你需要重构、升级依赖、添


Git Worktree: AI 编程 Agent 并行开发的秘密武器
陈佬昔编程人生2026/5/1

你在 AI 编程工具里开发一个新功能,突然产品过来让修复一个紧急 bug。于是你开了两个 AI Agent: Agent A:在 feature/new-dashboard 上写新功能 Agent B:在 fix/login-bug 上修一个登录 Bug 你心想:"两个 Agent 同时干活,效率翻倍。" 但三分钟后你回到编辑器,看到的是一幅这样的画面: app/ ├── dashboard.tsx ← Agent A 刚改了这里,但没写完 ├── login.tsx ←


我把 Hermes 里的模型几乎测了一遍,得出一个很扎心的结论:越贵的,往往越强
孟健AI编程2026/4/23

大家好,我是孟健。 这几周我在 Hermes 里来回切了很多模型。真跑下来,我越来越确认一件事:模型的水平,很多时候早就写在价格里了。把性价比榜倒过来看,八九不离十就是质量排行。 这不是 benchmark 结论。 是我把 Hermes 当生产底座,拿它去跑多 Agent、长流程、代码任务、资料整理之后,交出来的体感排序。 01 先给排序:贵,很多时候不是乱贵 先看这张图。 图里是按价格排的:便宜的在前,贵的在后。 但我这轮实际测下来,如果你把它倒过来看,它反而更像质量榜。 我的主观体感


c++从入门到跑路——string类
小肝一下2026/4/14

c++从入门到跑路——string类 1.为什么学习string类? 1.1 C语言中的字符串 C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列 的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户 自己管理,稍不留神可能还会越界访问。 1.2 两个面试题(暂不做讲解) 把字符串转换成整数_牛客题霸_牛客网 415. 字符串相加 - 力扣(LeetCode) 在OJ中,有关字符串的题目基本以stri


火爆全网的Seedance2.0 十万人排队,我2分钟就用上了
AI袋鼠帝2026/4/6

大家好,我是袋鼠帝。 之前我在B站看到一位AI视频创作者分享他的工作流。不可否认,那套流程做出来的视频确实很专业,画面精美,运镜流畅。但是,看完我只觉得头皮发麻。 原文档找不到了,我记得他先是用Gemini写剧本,接着用NanoBanana跑画面,然后再去另外的配音平台搞音频,中间穿插着使用ComfyUI来控制视频、图片生成。 ComfyUI这玩意儿我以前也折腾过几次,连线复杂就算了,每个节点的各种配置参数直接给我整懵逼了,我感觉比当初学敲代码还难,后面就再也没碰过了。 然后整个流程的最后一步,


Vue项目打包为WAR文件部署Tomcat完整指南
蒙眼过河2026/3/28

Vue项目打包为WAR文件部署Tomcat完整指南 前言 在Vue项目开发完成后,通常我们会将打包后的静态文件部署到Nginx等静态服务器上。但在某些企业环境中,我们需要将Vue项目部署到Tomcat这样的Java应用服务器中。本文将详细介绍如何将Vue项目的打包文件转换为标准的WAR包,以便部署到Tomcat服务器。 为什么需要将Vue打包为WAR包? 企业规范要求:很多企业使用统一的Tomcat应用服务器集群统一管理:便于与后端Java应用统一部署和管理历史遗留系统:部分老系统架构需


Django 基础入门教程(第四篇):Form组件、Auth认证、Cookie/Session与中间件
冉成未来2026/3/20

在前三篇中,我们完成了 Django 的环境搭建、模型设计、视图模板、Admin 后台以及 ORM 高级查询。本篇将带你深入 Django 的用户交互与安全机制:Form 组件、Auth 认证系统、Cookie/Session 和中间件。学完本篇,你将能够处理复杂的表单验证、实现用户注册登录、管理用户会话,并理解 Django 的请求/响应处理流程。 第一部分:Django Form 组件 1.1 为什么需要 Form 组件? 在 Web 开发中,处理表单是常见且复杂的任务。你需要:


AI时代的数据对比:DBA还需要盯着屏幕看差异吗?
NineData2026/3/12

当 AI 已经能写 SQL、辅助诊断、生成代码时,很多企业的数据对比却还停留在相对原始的阶段:任务跑完,DBA 需要面对动辄上百张表的差异报告,逐行核对的工作量极大。 这种场景在迁移、同步、数据备份演练里并不少见,到了国产化迁移场景下更是被进一步放大。数据库从 Oracle 迁到达梦、从 MySQL 迁到人大金仓,变化的不只是运行环境,更是数据库内核、数据类型、字符集规则和兼容语义。DBA 担心的往往不是任务失败,而是任务看起来已经完成,业务流量切换之后才发现数据并不一致。 AI 时代的数据对


ubuntu应用深度守护
字节逆旅2026/3/4

二、 定位分析:抽丝剥茧 1. 系统日志中的“启动死循环” 输入sudo grep "linux-myApp" /var/log/syslog调取 syslog 发现,系统曾多次尝试自动拉起应用,但均告失败。 报错核心:Exec binary ... does not exist: No such file or directory。 结论:系统预设的自动启动路径与实际安装路径不匹配,导致应用在服务器重启后无法“回家”。 2. 定位原因 上面的日志内容意味着我的应用可能已经被卸载、被移动了位


【C++】整数类型(Integer Types)避雷指南与正确使用姿势
PAK向日葵2026/2/24

背景 C++继承自C语言。作为一门以零开销抽象为主要特征的底层语言,不同于Python或JavaScript等高抽象层次的语言,C++拥有一套较为完整、但又包含有一定历史包袱的内建整数类型。 在实际开发中,如果对C++内建整数类型的机制不熟悉,或者不遵循一定的使用规范,则非常容易引入难以排查和调试的Bug。因此学习了解C++中内建整数类型的特性,以及一套行之有效的使用规范,是非常有必要的。 内建整数类型的坑 or 历史包袱 C++ 标准没有规定具体位数 虽然在实际实践中,我们知道在x64平台,对

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读