Redis单线程为何能支撑10万QPS?高并发架构核心拆解 📅 发布时间:2026/9/9 7:23:26 👁 浏览次数: 最近在技术群里被问到最多的一个问题就是Redis 明明是单线程的凭什么能扛住 10 万 QPS很多刚接触 Redis 的开发者一听到“单线程”这个词下意识就觉得它和高并发不搭边甚至有人问我“是不是 Redis 内部用了多线程但对外宣传是单线程”。其实 Redis 能跑到 10 万 QPS并不是靠堆线程堆出来的恰恰是因为它在设计上做了极其精准的取舍把“单线程执行模型”这个看似劣势的特性变成了一套非常高效的工作方式。这篇文章作为“Redis 实用技巧”系列的架构解析上篇我会从事件循环、内存存储、数据结构、命令执行链路这几个角度把“单线程 Redis 为什么快”这件事彻底讲明白。不管你是准备面试、还是在生产环境排查性能问题又或者只是单纯好奇 Redis 的内部实现这篇都值得花几分钟看完。看完你会发现所谓高并发真正靠的不是“并行”而是“减少不必要的工作”。1. 为什么单线程反而是个优点先看懂设计者的账本1.1 你看到的瓶颈和 Redis 实际面临的瓶颈不是一回事大多数时候我们聊多线程之所以觉得“线程越多越好”是因为大家都默认任务会吃满 CPU。比如图片处理、视频转码、复杂计算这类任务属于 CPU 密集型多核确实能带来立竿见影的收益。但 Redis 的核心场景是内存读写操作的是已经存在内存里的数据CPU 对单个命令的计算量非常小真正的时间往往花在网络 I/O、数据拷贝和上下文切换上。如果你把所有精力都放在“如何把 Redis 改成多线程”你会发现最直接的结果是线程多了锁也多了线程间为了抢共享数据互相等待CPU 频繁切换线程上下文缓存命中率下降性能不升反降。这是当年 Memcached 用多线程、但 Redis 坚持单线程的核心理由之一——在内存操作这个场景里串行执行不仅不慢反而更可控。1.2 省下三笔大开销切换、锁、缓存失效CPU 在处理一个线程时需要把当前线程的上下文保存下来再加载下一个线程的上下文这个动作就叫上下文切换。上下文切换本身是有代价的而且一旦线程数超过 CPU 核数切换会变得非常频繁。Redis 用单线程执行命令意味着命令执行过程里没有上下文切换CPU 可以专注于把当前命令处理完再处理下一个。锁竞争也是多线程模型里最头疼的问题。多个线程同时修改同一个哈希表、同一个链表必须有锁来保护否则会出现数据错乱。而单线程天然不需要锁因为命令是一条一条执行的不会有两个线程同时往同一个 key 上写数据。这带来的好处不仅是快更重要的是所有命令天然具备原子性——你不需要额外加事务执行 INCR、LPUSH 这类操作时中间不会被其他命令插进来。还有一个容易被忽略的点CPU 缓存。现代 CPU 有 L1、L2、L3 多级缓存单线程执行时热点数据很容易一直留在 CPU 缓存里读取速度极快。一旦多线程切换缓存里的数据会被反复淘汰每次都要回到内存里取数据反而慢了。1.3 单线程模型的隐藏红利确定性和可观测性多线程程序难调试因为执行顺序无法预知BUG 经常是“偶发”的。Redis 单线程模型下命令执行顺序就是客户端发送顺序任何时刻只会有一个命令在跑排查问题的时候只需要关注事件循环里发生了什么就能复现绝大多数性能问题。这也是我特别喜欢在生产环境里用 Redis 的原因之一——它的行为是高度确定性的出了故障可定位性很好。2. 核心细节拆解一条命令从进入到返回走过了一条什么样的路2.1 非阻塞 I/O 多路复用单个线程管上万连接的关键很多人有个误区觉得“单线程”就是一次只能服务一个客户端。实际完全不是这样。Redis 单线程能服务成千上万个连接靠的是I/O 多路复用。可以这样理解传统阻塞式网络模型里每个连接都需要一个线程专门等着数据没来就阻塞住线程白白浪费。而 Redis 的做法是一个线程同时盯着所有连接内核告诉他“哪个连接有数据可读”他就去处理哪个连接。Linux 下这个机制叫 epollmacOS 和 BSD 下叫 kqueueRedis 自己封装了一层事件驱动库屏蔽了不同操作系统的差异。整个工作流程是这样的Redis 主线程进入一个事件循环不断问内核“有没有新消息来了”如果某个客户端发来了一条命令事件循环就把这个连接标记为可读然后读取数据、解析命令、执行命令、把结果写到输出缓冲区再继续下一轮循环。整个过程里没有一个连接会阻塞主线程等待数据都是“有数据了就处理没数据就继续看别的”。所以 Redis 单线程真正在做的不是“一次服务一个请求”而是“一次服务所有请求中已经就绪的那一个”这个效率比每个线程盯一个连接高得多。假如有 1 万个空闲连接阻塞式模型需要 1 万个线程蹲守变成多路复用后一个 Redis 进程就够了。2.2 内存里的数据天生就快一次磁盘 I/O 的时间能做几百万次内存访问Redis 之所以能有十万级 QPS最根本的物质基础还是它把数据全部放在了内存里。内存随机访问的延迟一般在 80 纳秒到 100 纳秒级别而普通 SSD 随机读的延迟在 20 到 100 微秒机械硬盘更是在毫秒级别。也就是说内存访问比 SSD 快几百倍比机械硬盘快几万倍。这就出现了一个很关键的设计取向Redis 牺牲了“数据持久化绝对可靠”这一点把绝大部分时间都省下来服务请求。磁盘只负责定期把内存里的数据落盘或者以追加日志的方式记录写操作而且这些操作都尽量做成异步不让主线程等着。换句话说一个 Redis 请求在内存里很快就完成了磁盘 I/O 都靠后台线程或者子进程去处理主线程自然有能力不停接收新请求。2.3 底层数据结构设计每个命令都尽量做到 O(1) 或者 O(log N)内存快是一方面数据结构设计也得跟上。Redis 内部并没有直接用原生的字符串、链表、哈希表去存储数据而是针对自己的使用场景做了大量定制。比如字符串用的是 SDSSimple Dynamic String获取长度是 O(1)而且可以避免 C 语言字符串里常见的缓冲区溢出问题哈希对象底层有 ziplist 和 hashtable 两种编码小数据用紧凑结构大数据自动切换有序集合底层用跳跃表加哈希表让 ZRANGE、ZSCORE 这些操作都保持高效的复杂度。正因为每种数据类型背后都有一套针对性优化的结构Redis 才能做到大部分命令都是 O(1) 或者 O(log N)。你想想如果执行一个 GET 还需要遍历链表就算放在内存里也不可能跑出十万 QPS。所以“快”不是一个单点结果而是每一层都做了极致优化的综合结果。2.4 轻量级通信协议解析快、传输省Redis 的客户端和服务器之间用的是 RESPRedis Serialization Protocol协议。这个协议非常简单只有几种类型比如简单字符串、错误、整数、批量字符串、数组。命令就是数组数组里的每个元素都是批量字符串。因为格式固定、没有复杂的语义解析Redis 解析一条命令的开销非常低CPU 大部分精力都能留给真正的数据操作。这点和关系型数据库相比尤其明显。一个 SQL 要经过词法分析、语法分析、生成执行计划等一系列流程解析代价非常高。Redis 的协议简单到可以手写省掉的解析时间积少成多在高并发场景下差别很大。另外RESP 协议还有内联命令格式和管道支持客户端可以一次发送多条命令减少网络往返这也是吞吐量提升的重要途径。3. 实测一次10 万 QPS 到底怎么跑出来的3.1 先用 redis-benchmark 看一组直观数据Redis 自带的 redis-benchmark 工具是测试 Redis 性能最直接的途径。我在一台普通测试机上跑过大量测试单实例、纯内存、不开启 AOF、走本机回环地址SET/GET 这类简单命令的 QPS 基本都能稳定在 10 万以上。比如下面这条命令redis-benchmark -h 127.0.0.1 -p 6379 -t set,get -n 1000000 -c 50 -P 16参数含义-t指定测试命令-n指定总请求数-c表示并发连接数-P表示管道批量大小。把管道参数调大吞吐量会非常可观因为每个 RTT网络往返时间能带上 16 条命令网络等待被摊薄了。不过这里要提醒一句redis-benchmark 的数字是理想值不等于生产环境真实值。真实业务里命令多样有慢查询有大 value有持久化开销还隔着真实的网络链路不能拿 benchmark 结果直接当作容量评估依据。但它可以帮助你了解这台机器的 Redis 性能上限也能帮你验证配置是否正常。3.2 单线程事件循环在高并发下的真实处理节奏假设现在有 1000 个客户端同时连上了 Redis事件循环不会挨个去询问“你有没有消息”而是通过 epoll 一次性拿到“就绪事件列表”。这就好比一个接待员同时守着一排窗口哪个窗口有客户来了他就过去接待没人来的窗口他根本不需要管。每个命令的处理时间非常短往往只有几十微秒所以一秒钟内他可以来回处理几万个事件。需要注意的一点是Redis 的“单线程”指的是命令执行线程是单个的并不代表整个进程只有一个线程。Redis 在后台还有用于持久化的线程、用于异步删除大 key 的线程、用于关闭文件描述符的线程等。从 Redis 6.0 开始还增加了多线程 I/O就是把 socket 读写这部分工作从主线程里面拆出去让多个线程并行处理“网络数据的读取和写回”但最终的命令执行依然是在主线程串行完成。用一句好记的话总结Redis 6.0 的“多线程”只是让网络收发更快命令执行还是“一个一个来”。这样设计的好处是既能利用多核优势处理海量网络连接又保留单线程执行模型带来的原子性和确定性是最稳妥的折中方案。3.3 真正决定 QPS 的并不只是 Redis 本身还有客户端和网卡再快的 Redis如果客户端不会用也发挥不出来。实际压测时你会发现QPS 上限常常卡在网络和客户端侧。比如启用了 TCP Nagle 算法小包会被合并后才发送延迟增加整体吞吐下降比如客户端频繁创建销毁连接握手开销占了大头再比如单条连接上一条一条发命令没有使用 pipeline每发一条都要等一个 RTT。生产环境里想让 Redis 跑出高 QPS我的建议是连接池必须要用连接复用是基础高吞吐场景尽量启用 pipeline把多条命令打包发送关闭 Nagle 算法让数据包尽量立即发送。另外网卡中断处理、CPU 频率、内存频率都会影响最终数字。10 万 QPS 从来不是 Redis 单方面的事而是 Redis、网络、客户端三方配合的结果。下面这张表是我整理的不同场景下QPS 差异的主要来源影响因素低 QPS 场景高 QPS 场景网络模型每请求一连接长连接 连接池命令发送单条发送pipeline 批量发送TCP 参数默认 Nagle关闭 Nagle 降低延迟命令类型复杂命令、大 key简单命令、小 value持久化配置AOF alwaysAOF everysec 或关闭4. 单线程模型下的避坑指南这些操作千万别乱用4.1 慢命令是单线程模型最大的敌人没有之一因为所有命令都是串行执行的一条命令耗时长后面所有命令都得等它。单线程最怕的就是慢命令。真正慢的命令往往不是 O(N) 那么简单而是那些把大量数据一次性取回来或者一次性删除的操作。比如 KEYS 命令它需要遍历整个键空间匹配模式数据库里如果有几百万个 key执行一次就能把 Redis 卡住几百毫秒线上服务基本就雪崩了。正确的替代方案是使用 SCAN 命令游标式地分批遍历每次返回少量 key不会长时间阻塞。类似的HGETALL 用于大哈希、SMEMBERS 用于大集合、LRANGE 用于大列表也要尽量避免。如果确实需要取全量数据要么缩小小 key 的规模要么分批次取。我在生产环境见过最典型的案例代码里用 KEYS 做缓存清理业务量一上来Redis 每隔几分钟卡一次。后来改成 SCAN 分批删除问题立刻消失。4.2 大 Key 的杀伤力往往是延迟和内存的双重爆炸大 key 指的是单个 key 对应的 value 特别大比如一个哈希里有几十万个字段一个列表里有几百万个元素。这种 key 的危害一是任何针对它的命令都可能耗时很久阻塞整个 Redis二是内存占用高影响持久化和复制效率三是删除的时候如果直接 DEL同样会阻塞主线程。排查大 key 可以直接用 Redis 自带的工具redis-cli --bigkeys它会扫描整个实例按数据类型统计出最大的 key 有哪些。对于确实存在的大 key删除时不要用 DEL可以异步删除UNLINK mykeyUNLINK 只在主线程里做很小的整理动作真正的内存释放交给后台线程。这是 Redis 4.0 之后我非常推荐的操作能有效避免删除大 key 造成的阻塞。4.3 阻塞式命令要谨慎别让“等”变成“卡”Redis 里还有一类命令设计初衷是“阻塞等待”比如 BLPOP、BRPOP。客户端执行 BLPOP 时如果列表为空它会一直阻塞到超时或者有新数据进来。这个阻塞等待本身并不会占用 CPU但是一旦有多个客户端同时在阻塞Redis 需要维护这些等待关系生产环境中如果一个列表的消费速度跟不上生产速度等待的客户端会越来越多事件循环压力也会变大。尤其要注意的是阻塞命令如果在主线程上执行且参数不设置超时客户端可能长时间占着一个连接不释放连接数被大量消耗最终影响其他正常请求。所以要给阻塞命令都配上合理的超时时间并且充分评估消费能力不要让生产速度和消费速度严重失衡。4.4 持久化也尽量别抢主线程的时间很多人以为 Redis 开了持久化就是每次写操作都刷磁盘。事实并非如此具体要看配置。RDB 快照是 fork 一个子进程去生成快照文件主线程继续服务fork 瞬间会有一次阻塞AOF 的写盘频率由 appendfsync 决定如果配置成 always每条命令都 fsync性能会显著下降。推荐配置是 appendfsync everysec每秒刷一次既保证安全性又把对主线程的影响降到最低。另外Linux 内核的 fork 机制采用写时复制Copy-On-Write正常情况下内存占用不会翻倍。但如果开启了大量写操作子进程持有了快照父进程修改内存页时就会触发复制内存占用会上升需要提前留足内存余量。生产环境中我见过因为内存不足导致 fork 失败Redis 直接拒绝写入的情况所以不要忽略了持久化带来的隐性内存开销。4.5 延迟排查三板斧SLOWLOG、LATENCY、redis-cli --latency当你怀疑 Redis 变慢时别急着盲目优化先定位问题。先看慢日志SLOWLOG GET 20慢日志会记录执行时间超过阈值的命令默认阈值是 10000 微秒也就是 10 毫秒。生产环境建议把这个阈值调低一些比如 5000 微秒能更早发现隐患。命令执行超时通常是慢命令、大 key、或者 CPU 争抢导致的可以直接从 SLOWLOG 里找到元凶。如果慢日志里找不到明显问题可以使用 Redis 提供的延迟监控工具redis-cli --latency这个命令会持续采样显示网络往返延迟。如果延迟出现明显尖峰结合慢日志和系统监控基本就能定位是 Redis 内部问题还是网络问题。还有一个经验是查看INFO commandstats统计每个命令的调用次数和耗时占比很多“隐藏的慢命令”在总量上不明显但平均耗时很高能从 commandstats 里看出来。一些个人经验收个尾做了这么多年缓存和中间件相关的项目我对 Redis 的体会是它的快不是魔法而是一套清晰的“减法哲学”——用内存替代磁盘用事件驱动替代阻塞等待用高效数据结构和简单协议减少每个请求的 CPU 开销用单线程换掉锁和上下文切换的代价。这套设计在大多数业务场景下都非常合理因为大多数请求本来就是微秒级的小操作瓶颈从来不在 CPU而在网络和客户端。我也踩过不少坑。早期接手一个项目时发现 Redis 的 QPS 上不去第一反应是加机器、加内存后来用 SLOWLOG 一看满屏都是 KEYS 和 HGETALL 扫大哈希把执行时间从微秒级拖到了几十毫秒。把代码改成 SCAN 和小批量处理之后单实例 QPS 直接翻了两倍多。所以说理解单线程模型的应用边界比盲目升级硬件更值钱。这篇文章侧重架构层面的设计原理后面如果再聊“下”篇我打算结合实例讲讲如何通过监控、调参和优雅的客户端用法把 Redis 每个 10 万级 QPS 长期稳定在可控范围内。