【Redis】网络高并发模型

目录

    • [一、 核心场景:百万并发下的秒杀大考](#一、 核心场景:百万并发下的秒杀大考)
    • [二、 深度对比:多线程阻塞 vs 单线程非阻塞](#二、 深度对比:多线程阻塞 vs 单线程非阻塞)
    • [三、 演进:Redis 6.0+ 的多线程 I/O 革命](#三、 演进:Redis 6.0+ 的多线程 I/O 革命)
    • [四、 微观视角:一个秒杀请求的时间线拆解](#四、 微观视角:一个秒杀请求的时间线拆解)
    • [五、 致命死穴:如果某个命令很慢怎么办?](#五、 致命死穴:如果某个命令很慢怎么办?)
    • 本篇总结

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

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

bash 复制代码
SET stock 1

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

python 复制代码
if stock > 0:
    stock = stock - 1
    return success
else:
    return fail

⚠️ 注意 :在真实生产环境中,该操作必须使用 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 再将剩余数据异步写回客户端。整个过程不会因为某个客户端的"网速慢"而阻塞其他客户端。

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

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

二、 深度对比:多线程阻塞 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 到接收响应的微观全过程:

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

💡 小结 :单个请求的闭环处理大约需要 120   μ s 120\,\mu s 120μ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 仅仅优化了"网卡到缓冲区"的搬运速度,其灵魂(核心执行线程)依然纯粹单线程。
相关推荐
我是一颗柠檬1 小时前
【Redis】列表与集合Day4(2026年)
数据库·redis·后端·缓存
AOwhisky1 小时前
Ceph系列第三期:Ceph 集群核心配置与管理
linux·运维·数据库·笔记·ceph
陈天伟教授1 小时前
安装 AutoCAD 时,“可选工具“ 的详细说明。
数据库
2401_892423361 小时前
OSPF实验
网络
koo3641 小时前
周报5.31
网络
zcn1261 小时前
举一反三思路思考形如(列=参数 or decode函数)
数据库·sql优化改写
Devin~Y1 小时前
从内容社区到AIGC客服:Spring Boot、Redis、Kafka、K8s、RAG的三轮大厂Java面试对话(附标准答案)
java·spring boot·redis·spring cloud·kafka·kubernetes·micrometer
それども1 小时前
怎么理解TCP的状态
java·网络·网络协议·tcp/ip·dubbo
Xzh04231 小时前
Redis黑马点评 实战复盘与面试高频考点详解
java·数据库·redis·面试