
Redis为什么这么快这个话题几乎每个后端同学在面试前都背过而只要聊到快就一定绕不开 Redis 的线程模型尤其是 6.0 之后被反复讨论的 “Redis 多线程”。说实话网上讲这题的文章不少但大部分版本要么停留在 “Redis 是单线程的” 这个旧结论上要么把多线程吹成 “Redis 终于支持并发执行命令了”这两种说法都不准确而且很容易误导人。这篇文章我想把整条线完整串一遍快在哪三层底层支撑、单线程模型内部到底怎么工作、6.0 引入的多线程改了什么又没改什么以及线上 Redis 变慢时怎么用最快的路径定位问题。适合正在准备面试、做中间件选型、或者被生产环境慢查询坑过的朋友参考。1. 先搞清楚Redis 的“快”到底来自哪三层很多人一说 Redis 快张口就是 “因为它基于内存”。这个答案没错但太粗了。Redis 快是一个系统工程至少三层叠加出来的结果内存存储、高效数据结构、事件驱动模型。缺了哪一层都不可能做到单机十万甚至几十万的 QPS。1.1 第一层数据住在内存里这是快的地基这一层最好理解。内存随机访问的耗时大概在几十纳秒到一百纳秒这个量级而普通 SSD 的随机读耗时是几十微秒到几百微秒传统机械硬盘更是直接到毫秒级。纳秒、微秒、毫秒每跳一个单位就差三个数量级内存比机械硬盘快了三到四个数量级比 SSD 也快了两三个数量级。打个比方内存访问就像你直接走到食堂窗口师傅把餐盘递给你SSD 相当于你去便利店得走过去、扫码、付款而传统磁盘就像你点了外卖得等商家备餐再等配送。Redis 把全部数据放在内存里天然就赢在了起跑线上。不过这里得泼一盆冷水Redis 把所有数据放内存意味着成本很高。同样 1GB 数据放内存可能比放 SSD 贵几十倍所以 Redis 不适合做全量冷数据存储它的定位从来都是缓存和热数据加速层。这也是为什么后来有了 SSD 上的存储引擎但那是另一个话题。至少在用 Redis 的时候你得想清楚这份数据值不值得占内存。1.2 第二层数据类型背后的数据结构不是拍脑袋选的面试常问的 Redis 数据类型就那五种String、Hash、List、Set、ZSet。热词里也反复出现 “redis 数据类型”但很多人记了命令却忽略了每种类型底层的数据结构这才是 Redis 快的一个关键点。String 底层用的是 SDS简单动态字符串而不是 C 语言裸字符串。SDS 记录了字符串长度获取长度是 O(1)而且能避免缓冲区溢出修改字符串时按需扩容。Hash 在元素少时用 ziplist 或 listpack 压缩存储节省内存元素多了之后转换成哈希表读写还是 O(1)。这里有个渐进式 rehash 的设计扩容搬数据不是一次性完成而是分摊到每次增删改查避免了长时间阻塞。List 在 3.2 之前是 ziplist linkedlist3.2 之后用 quicklist 把两者结合既省内存又保证两端操作是 O(1)。7.0 开始又用 listpack 优化。ZSet 用跳表 哈希表实现。跳表让范围查询和按分值排序都接近 O(log N)哈希表保证按 member 查询分值是 O(1)。Set 底层是 intset 或哈希表整数集合用紧凑数组内存和速度都优秀。为什么要专门强调数据结构因为同一份数据你用 List 存和用 ZSet 存做范围查询的复杂度完全不一样。Redis 快不只是 “内存里查得快”还在于各种操作都被设计成 O(1) 或 O(log N)很少出现 O(N) 的拖油瓶。这也是它敢用单线程跑命令的底气之一。1.3 第三层I/O 多路复用与无锁设计决定了并发上限内存和数据结构解决的是单次操作快不快但一个服务器要同时面对几十万个客户端连接如果每个连接分配一个线程去处理线程上下文切换开销就能把 CPU 打满。Redis 选择的是 I/O 多路复用机制在 Linux 上就是 epoll一个线程可以同时监听成千上万个 socket 上的读写事件哪个连接来了数据就处理哪个没有数据就休眠等待CPU 利用率极高。这里有一个特别重要的推论因为命令执行是单线程的多个命令之间不需要加锁也不存在竞争条件。这省掉的不是一星半点开销。多线程并发编程里锁竞争、上下文切换、缓存行失效每一项都是性能杀手。Redis 相当于用 “串行执行命令” 换来了绝对的并发安全同时又用 epoll 把并发连接的处理能力拉满。三层合在一起才能说清楚 “Redis 为什么这么快”。只讲内存解释不了高并发下的连接处理只讲 epoll解释不了单条命令为何这么快。这也是面试时候比较加分的回答方式不是背结论而是拆层次。2. 单线程到底在干什么一次命令背后的完整链路既然标题是 “线程模型”那必须把单线程模型聊透。很多文章反复说 “Redis 单线程高性能”但到底是怎么个单线程法背后的事件循环是什么样很多人其实没完全搞明白。2.1 先破除一个误解Redis 单线程不是说进程里只有一个线程这是我在面试里经常反问候选人的点。Redis 进程里其实有不少线程和子进程比如主线程负责处理命令请求。后台线程负责 AOF 刷盘、懒释放大 key、以及 4.0 之后的一些后台删除任务。fork 出的子进程RDB 持久化、AOF 重写会 fork 子进程来做。从 6.0 开始还有可选的 I/O 读写线程。所以 “Redis 是单线程” 这句说全了应该是Redis 处理命令执行的线程只有一个所有客户端发来的命令在这个主线程上排队、依次执行不存在命令并行执行。而网络 I/O、文件 I/O 这些操作早就有独立的线程或子进程在帮忙了。这个澄清很重要因为它在后续分析性能瓶颈、排查问题的时候决定了思路。比如有人误以为 Redis 支持多线程命令执行于是写一堆 CPU 密集型操作丢进去结果发现 Redis 还是卡那就是对模型理解错了。2.2 事件循环 epoll一个线程怎么伺候几万个连接Redis 主线程的核心是一个事件循环不断做两件事等待事件、处理事件。这里的事件分两类文件事件socket 上可读、可写。这是命令请求和响应事件。时间事件定时任务比如过期键清理、每秒统计。文件事件由 I/O 多路复用器监听。什么是 I/O 多路复用简单说原来你要等 100 个客户端发数据如果一个连接一个线程就要 100 个线程现在一个线程调用 epoll_wait内核帮它盯着这 100 个 socket只要有任何一个可读或可写就返回一个就绪列表线程一个接一个处理处理完再回到 epoll_wait 继续等。用餐厅打比方单线程模式不是一个服务员只服务一桌客人而是一个服务员站在大厅中间同时关注所有桌子的动静哪桌举手就过去服务服务完回到大厅中间继续观察。这个服务员当然不可能同时给两桌炒菜但翻台速度极快所以整体服务能力并不差。Linux 上 epoll 之所以厉害是因为它内部用红黑树维护需要监听的 fd用就绪链表记录发生事件的文件描述符内核只返回有事件的那些 socket而不是像 select/poll 那样每次把上万 fd 全扫一遍。Redis 的源码里在 ae_epoll.c 中就是对这个机制做了一层封装。其他平台上也有对应实现Mac 上是 kqueueWindows 上没有 epoll只能用 select 或者 IOCP 之类的方案这也是为什么网上搜 “redis windows 下载” 出来的 Windows 版本性能一般而且官方一直是建议生产环境跑在 Linux 上。2.3 一条 GET 命令在内核和用户态之间的完整流转我特别喜欢把一次命令的完整路径画出来讲这样最能体现单线程模型的工作方式。以最简单的 GET 为例客户端发送GET key到达服务器的网卡内核把数据放进对应 socket 的接收缓冲区。Redis 主线程调用 epoll_wait内核发现这个 socket 可读返回就绪事件。Redis 从事件循环中取出该文件事件调用 read 从 socket 读入请求数据。解析协议。Redis 用的是 RESP 协议TCP 流上可能是多条命令粘在一起所以解析器要能处理粘包、半包。解析出命令后在命令表里找到 getCommand 函数执行查找 key 并返回 value。把结果按 RESP 协议编码写入当前 socket 的发送缓冲区注册写事件。回到 epoll_wait 继续等待同时内核把发送缓冲区的内容通过 TCP 返回给客户端。注意第 5 步在主线程上执行它是严格串行的。也正因为串行Redis 在处理命令时不需要考虑两个客户端同时修改同一个 key 的竞争问题。这一整个流程里真正消耗 CPU 的部分主要是第 3、4、6 步的读写和协议解析而第 5 步通常是内存里的哈希查找非常快。这也就能解释为什么 Redis 在吞吐量上可以做到单实例十几万 QPS它把大量 CPU 资源集中在一件事上没有锁、没有上下文切换、没有调度开销剩下的就是纯网络回环 内存读写。2.4 单线程的代价哪些命令会把整个 Redis 拖垮了解了单线程执行模型你就能推出一个结论一旦某个命令执行时间很长后面所有命令都得排队等它。这是 Redis 最典型的 “宕机式变慢” 场景原因通常是下面几类一次性返回大量数据的命令比如KEYS *在几百万 key 的实例上会阻塞主线程好几秒。正确做法是用SCAN分批遍历或者干脆避免。操作大 key比如一个 List 有几十万条数据直接LRANGE key 0 -1或对超大 Hash 执行HGETALL不仅主线程卡住网络传输也是大问题。聚合和排序类命令SORT、ZUNIONSTORE这类命令如果操作的集合很大耗时会非常明显。不合理的过期策略如果同时有大量 key 在同一秒过期Redis 周期性的过期清理会花不少时间扫描删除造成时间事件挤压。fork 阻塞RDB 持久化或者 AOF 重写会 fork 子进程内存越大 fork 耗时越长期间主线程也会被短暂挂起。所以单线程模型虽然好但要求使用者必须自律。Redis 官方一直强调 “Don’t run slow commands”这不是开玩笑是血泪教训。后面我会专门讲排查慢命令的方法。3. Redis 6.0 的多线程到底改了什么聊完单线程的机制和代价就到了标题的重点Redis 多线程。这里需要非常明确地说一件事Redis 6.0 引入的多线程不是多线程执行命令而是多线程处理网络 I/O。这个区别如果不搞清楚后面全是一笔糊涂账。3.1 为什么单线程这么香Redis 还要引入多线程既然上面把单线程说得这么好为什么还要改答案很简单随着连接数和单次请求体积上升网络 I/O 开始成为瓶颈而 CPU 还没跑满。Redis 的瓶颈从来不是 CPU 算力而是网络读写和协议解析。早期单线程模型里主线程既要 accept 新连接又要 read 请求数据还要 parse 协议、执行命令、write 回包。当客户端数量多、单个请求的 value 很大比如几 KB 甚至几十 KB 的对象或者使用 pipeline 批量请求时read write 协议解析会占用大量主线程时间主线程被网络 I/O 拖住真正执行命令的时间占比反而下降了。打个比方一个厨师主线程本来是炒菜的执行命令现在餐厅生意好他还得自己去菜市场进货read、洗菜切菜解析协议、把菜端上桌write忙完这一圈真正炒菜的时间没多少了。多线程 I/O 就像是加了几个帮厨进货、洗菜、端菜都有专人干厨师只专注炒菜。所以 Redis 官方在 6.0 引入多线程 I/O 的目标很明确把 read、write、协议解析这些网络操作挪到独立的 I/O 线程上让主线程把精力放在命令执行上同时仍然保持命令执行阶段的单线程串行。3.2 多线程 I/O 的工作机制读线程、写线程和主线程怎么分工Redis 6.0 的 I/O 多线程并不是把连接固定分配给某个线程去处理而是一个按事件分发的协作模型。简化概括一下主线程负责 accept 新连接然后在第一次读事件把某个客户端 socket 分配给一个 I/O 线程。I/O 线程负责从 socket 中读取数据、解析出完整的命令对象放入队列。主线程从队列中取出命令逐个执行。这一步依然是单线程、串行、无锁。命令执行完成后主线程不直接写回而是把响应结果挂在队列上由 I/O 线程负责写回客户端。你可能会问既然命令执行还是单线程那多线程 I/O 到底有什么意义意义就在于在高并发读多写少的业务下read 和 write 的耗时可能占整个请求处理的一半甚至更多。把这部分时间分散到多个线程主线程就能腾出来执行命令整体吞吐自然就上去了。而且因为命令执行还是串行的Redis 不需要为 key 的读取和修改加锁不存在数据竞争问题一致性依然和单线程一样干净。这是整个设计里最天才的一点在不破坏单线程执行模型的前提下把并行的收益吃到了网络层。3.3 配置与实测io-threads 该开多少才合理Redis 6.0 之后redis.conf 里多了两个关键配置# 开启 I/O 多线程默认不开启设置为大于 1 的值才生效 # 建议最多 8 个按 CPU 核数调整 io-threads 4 # 是否开启读线程默认 no开启后读请求也由 I/O 线程处理 io-threads-do-reads yes这里有几个实测下来的感受io-threads不是越大越好。官方建议一般 2 到 4 就够极端场景 8 也够用不建议超过 8。因为 I/O 线程之间还有任务分配和队列同步的开销线程太多收益反而下降。io-threads-do-reads默认是 no如果你不是特别明确的流量模型建议先不开。开了之后读请求的解析会从主线程挪到 I/O 线程但如果你的业务大部分请求都是几字节的小 key这点收益其实很小。这个配置必须重启 Redis 才生效不能热修改。开启前最好用redis-benchmark或者真实业务流量做压测对比。我见过有人配置完 io-threads 8结果因为网络包太小、QPS 反而不如单线程的案例。不要盲目拍脑袋。从实际经验看多线程 I/O 最适合的是单个 value 比较大、客户端连接数多、存在 pipeline 批量请求的场景。如果你就是一台普通的 4 核 8 核机器业务以小 key 为主开不开差别可能不大偶尔还会因为线程调度带来轻微延迟波动。3.4 多线程不是万能药什么时候开了反而没收益这一段是纯经验之谈也是面试里很容易被追问到的问题。Redis 多线程 I/O 能提升性能但它的收益边界非常明显如果瓶颈是 CPU 本身比如你的命令执行链路里有大量复杂操作、有大 key 遍历那么多线程 I/O 帮不了忙因为命令执行还是单线程串行的该卡还是卡。如果单次请求的 value 很小、网络包都是几十字节那么 read/write 耗时占比极低多线程 I/O 收益有限因为主线程大部分时间本来就在执行命令而不是等网卡。如果部署机器的 CPU 核数很少比如只有 2 核你开 4 个 I/O 线程线程之间互相抢 CPU反而拖慢整体性能。如果开启了io-threads-do-reads但业务中读多写少且请求量大可能有任务分发不均的问题因为 I/O 线程读取完成后还是要等主线程逐个执行命令队列再长也只能排队。所以多线程 I/O 是在 “网络 I/O 挤占命令执行时间” 这个特定瓶颈上的补丁。它解决的是特定场景的短板不是让 Redis 变成多线程执行器。这也是面试官最爱问的一句话Redis 6.0 的多线程多的是 I/O不是命令执行。4. 排查 Redis 变慢的实操实录从现象到结论聊完理论进入实战。我在排查线上 Redis 变慢问题的时候基本有一套固定的流程不靠猜全靠看数据。4.1 第一步先确认 Redis 是“网络慢”还是“命令慢”遇到 Redis 变慢先别急着改配置。打开三个终端分别跑三条命令# 查看当前 OPS 和命令耗时统计 redis-cli info stats | grep -E instantaneous_ops_per_sec|total_commands_processed # 检测本机到 Redis 的网络延迟跑 10 秒 redis-cli --latency # 检测 Redis 进程本身的固有延迟受内核调度、虚拟化等影响 redis-cli --intrinsic-latency 100如果--latency很高但--intrinsic-latency正常说明问题在网络链路重点看客户端到服务端之间的网络设备、带宽、连接数。如果--intrinsic-latency很高说明问题出在 Redis 进程本身比如机器内存不够触发 swap、CPU 抢占、或者主线程被慢命令卡住。这一步能帮你把问题从 “整个链路” 缩小到 “Redis 自身”。4.2 第二步用 --bigkeys 和 commandstats 揪出大 Key 与热 Key如果确认是 Redis 进程自身慢最常见的元凶就是大 key 和热 key。直接扫# 扫描大 key redis-cli --bigkeys # 查看所有命令的调用次数和耗时占比 redis-cli info commandstats--bigkeys跑一遍会把每种数据类型里最大的几个 key 列出来。看到大 key 之后先确认业务是否真的需要这么大一个对象如果不需要就分拆、压缩或者把 value 换到对象存储。特别要注意的是这个命令本身也会遍历整个键空间建议在业务低峰期执行或者用 SCAN 手写脚本慢慢扫避免对线上造成影响。commandstats则告诉我们究竟是哪个命令在疯狂高频执行。之前我排查过一个案例某个业务在一个 for 循环里对同一个 key 调了上千次 GET单条命令都是微秒级但累计下来把主线程占满了。这种问题不是 Redis 本身慢是客户端用错了。另外真正常见的还有过期 key 集中清理。Redis 的过期键删除有个 lazy free 选项如果遇到大批 key 同时过期导致卡顿可以考虑用CONFIG SET lazyfree-lazy-expire yes把过期删除动作放到后台线程但这个只能缓解不是根治最好还是把过期时间打散避免集中过期。4.3 第三步看懂 SLOWLOG把慢命令拉到阳光下排查慢命令最直接的武器是 SLOWLOG。Redis 默认把超过 10000 微秒10ms的命令记录到慢日志可以这样查# 查看最近 10 条慢命令 redis-cli slowlog get 10 # 查看慢日志当前长度 redis-cli slowlog len默认 10ms 的阈值其实偏低配置线上建议你自己调整。比如对于核心缓存我希望任何命令都能在 1ms 内完成那可以把阈值改小现场抓慢命令# redis.conf 或 CONFIG SET 修改 slowlog-log-slower-than 1000 slowlog-max-len 256慢日志里最关键的是命令内容和耗时参数。看到LRANGE、SMEMBERS、HGETALL、KEYS这类命令上榜基本都不用犹豫直接找业务方改代码。如果你发现慢命令只是一些很普通的 GET/SET那就要结合当时的 OPS 和机器负载综合判断可能是瞬间流量太高或者内存碎片严重导致的换页。4.4 常见误区速查客户端多线程不等于 Redis 多线程这个主题下我强烈建议你存一份常见误区对照表因为很多人、包括一些工作几年的工程师都会在这些点上踩坑常见说法实际情况Redis 是单线程的所以无法并行处理请求服务端命令执行是单线程串行的但网络 I/O、持久化、异步删除都有线程/子进程参与客户端依然可以并发连接客户端开了多线程Redis 服务端也会多线程处理客户端多线程只代表并发发送请求服务端依然排队逐个执行瓶颈依然可能在服务端主线程Redis 6.0 支持多线程了所有命令可以并行执行6.0 的多线程只在网络读写和协议解析阶段生效命令执行依然是单线程机器核数多io-threads 配大点没问题io-threads 过大反而增加同步开销建议 2~8最多别超过 8还要结合实际压测只要不加慢命令单线程就永远不卡大 key、集中过期、fork 持久化、swap 内存不足都会造成主线程阻塞用 KEYS 遍历没多大问题在几百万 key 上 KEYS 可以阻塞主线程秒级必须用 SCAN这张表可以直接当面试和实战笔记用。我见过有人把客户端连接池从 50 调到 500 之后发现 Redis QPS 不升反降最后才发现是服务端单线程执行不过来连接多了反而增加上下文切换开销。这就是典型的没分清客户端并发和服务端并发。5. 面试和项目里怎么把“线程模型”讲明白这部分算是很多人的刚需。毕竟热词里 “redis 面试题”“多线程面试题” 常年挂在热搜上说明大家都在背这一题。5.1 3 分钟讲清 Redis 线程模型一套可复用的回答框架面试官问 “Redis 为什么快说说线程模型”理想的回答不是背一段八股文而是分四条线清晰推进先给结论快在内存、数据结构、IO 模型、无锁设计四个层面。讲清旧模型Redis 6.0 之前命令执行是单线程的通过 I/O 多路复用epoll监听海量连接事件循环内串行执行命令避免了锁竞争和上下文切换。转折到新模型6.0 之后官方发现网络读写与协议解析成了主线程瓶颈于是引入多线程 I/O把 read/parse/write 交给独立 I/O 线程主线程仍然只做命令执行。所以 Redis 的 “多线程” 是多在网络层不是多在执行层。补一句生产经验多线程 I/O 适合大 value、高连接数、pipeline 场景配置 io-threads 要按核数和压测结果来不是越大越好。同时一定要避开 KEYS、大 key、集中过期这些单线程模型下最容易翻车的操作。这套框架的好处是既有高度又有细节而且展示了你对版本演进和线上场景的理解。比生硬地背 “单线程 多路复用 内存” 三个词强得多。5.2 项目实战心得单线程模型下怎么把业务代码写好最后说点真实的项目体会。理解了线程模型之后你写业务代码的思路应该改变尽量用 pipeline 批量操作。客户端一次发送多条命令相比逐条发送能把多次 RTT 变成一次对主线程的 read/write 压力也小很多。需要原子性时用 Lua 脚本一条脚本在 Redis 内是原子执行的既保证并发安全又能减少多次 RTT。用 SCAN 代替 KEYS用 SSCAN/HSCAN 代替 SMEMBERS/HGETALL 全量获取。遍历要分批切忌一把梭。给 key 设置过期时间时加入随机偏移量避免大量 key 在同一秒过期。上线前最好用redis-cli --bigkeys扫一次把明显的大 key 问题在测试环境就暴露出来。监控上盯住 slowlog 和 instantaneous_ops_per_sec 这两个指标前者看有没有异常命令后者看流量变化是否符合预期。这些点是我在多次线上事故里一点点攒出来的。Redis 本身其实很皮实大多数“Redis 变慢”都是使用姿势的问题而不是 Redis 性能不行。线程模型决定了它的天花板而你怎么用决定了你能否够到这个天花板。回到标题那句话Redis 为什么这么快内存是地基数据结构是骨架I/O 多路复用和无锁设计是引擎6.0 的多线程 I/O 则是在保持单线程执行模型的基础上把网络 I/O 这块短板补上。理解了这个演进过程你以后再遇到 Redis 性能问题就不会只知道加内存和重启了。