Redis 为什么这么快?IO 多路复用 + 单线程 + pipeline 提速 108 倍,一次讲透

0 阅读8分钟

Redis 为什么这么快?IO 多路复用 + 单线程 + pipeline 提速 108 倍,一次讲透

作者:鱼宵 | 实战驱动系列 · 第 5 篇

完整课程与可运行源码已开源在 Gitee:gitee.com/j67mk2/redi… (10 课实战教程,本课源码在 lesson-08/)


一、真实场景:面试官问"Redis 为什么快"

十个面试者九个这么答:"因为 Redis 把数据放内存里,内存快啊。"

这话只对 1/3。内存快没错,但 MySQL 也有内存缓存、MongoDB 也吃内存,凭什么 Redis 能扛每秒十万+的读写?完整的答案是三板斧:

  1. 纯内存操作
  2. IO 多路复用(面试最容易讲懵、也最加分的一点)
  3. 单线程模型(听着反直觉,恰恰是快的原因之一)

外加一个"用得更快"的实战利器 pipeline——我实测过:同样 1 万条命令,普通写法 3914 毫秒,pipeline 只要 36 毫秒,快 108 倍。这篇文章把三板斧讲透,再把实测数据甩给你看。


二、板斧一:纯内存操作

数据都在内存里,读写就是改几个字节,纳秒~微秒级;磁盘要几毫秒。内存比磁盘快约 10 万倍。

但注意——内存是必要条件,不是充分条件。别的组件也用它,真正拉开差距的是下面两板斧。


三、板斧二:IO 多路复用

这是面试重点。用银行柜台类比:

  • 老式做法(BIO,阻塞 IO):每个来办业务的客户配一个专属服务员,这个服务员死盯着这一个客户——哪怕客户在翻钱包、填单子没事干,服务员也不能去接待别人。客户多了要招一大堆服务员,还都在干等。
  • IO 多路复用:一个前台经理(就是 Redis 主线程)同时盯着一堆窗口,谁的客户"有事了"(单子填好了、要办业务了),就叫谁过来。一个人盯 1 万个连接也不累。

Redis 底层用的是 epoll(Linux 上),它的精髓是"事件驱动":不是每次把 1 万个人全问一遍"你有事吗"(那是 select/poll 的笨办法,挨个轮询),而是只把"真有事的那些人"通知给前台。谁有事找谁,不做无用功。这就是 Redis 能扛住海量连接的关键。


四、板斧三:单线程模型(+ 6.0 多线程的真相)

Redis 执行"命令"这件事是单线程的。别一听单线程就觉得慢——它恰恰是快的原因:

  • 没有加锁解锁:多线程要抢锁、防竞争,Redis 不用,天生无锁;
  • 没有上下文切换:操作系统不用在线程之间来回切(一切换就是微秒级浪费,切几百万次就毁了);
  • 代码简单,没有并发 bug。

类比:一个厨师在后厨炒菜,同时开 10 个灶他要来回跑、切完这个菜洗手再切那个菜,反而慢;一个人专心炒,锅气还足。Redis 就是这个专心炒菜的厨师。

那 6.0 出的"多线程"不是打自己脸吗? 不是。6.0 的多线程只用来处理网络 IO(读 socket、写 socket),真正执行命令(GET/SET/ZADD)依然单线程、依然排队。

为什么要加?因为网卡越来越快,Redis 一秒要扛几十万个网络包,"读包""写回包"这俩体力活把单线程 CPU 占满了,命令反而排队等不及。把体力活分给几个 IO 线程干,命令执行还是一个人干——既提了吞吐,又没破坏单线程无锁的好处。

类比:厨师(命令执行)还是一个人,门口加了几个传菜员(IO 线程)专门收单子、端菜。厨师不用自己跑前跑后,专心炒菜。


五、pipeline:一趟车拉一车货

我们写 jedis.set(k, v) 时,背后是:Java 发一个包 → Redis 收包执行回包 → Java 收到回复。这一来一回就是一个 RTT(网络往返,哪怕本机回环也要零点几毫秒)。1 万次 set = 1 万个 RTT。

pipeline 的思路:把 1 万条命令在 Java 这边攒起来,一次性发给 Redis,执行完一次性收回复。

类比:普通循环是快递员一趟只送一件货,跑 1 万趟;pipeline 是快递员把一整车装满,一趟送完。

代码对比就两行的事:

// 普通循环:每条命令一次网络往返
for (int i = 0; i < TOTAL; i++) {
    jedis.set("normal:" + i, "value-" + i);
}

// pipeline:命令先在本地攒着,sync() 才一次性走网络
Pipeline pipe = jedis.pipelined();
for (int i = 0; i < TOTAL; i++) {
    pipe.set("pipe:" + i, "value-" + i);
}
pipe.sync();   // 1 万个 RTT 变成 1 个 RTT

两个使用要点(面试会追问):

  1. 省的是 RTT,不是 Redis 执行时间——适合"批量、彼此无依赖"的命令;
  2. 命令之间不能有依赖:你不能先 pipe.set(a, ???) 再根据 a 的值决定 b——sync() 之前你根本拿不到 a 的回复。

六、实测:pipeline 快 108 倍,bigkey 一眼揪出

完整工程在仓库 lesson-08/,mvn exec:java 一键跑。下面是本机真实运行输出:

[普通循环] 10000 次 SET 耗时 = 3914 ms
[pipeline ] 10000 次 SET 耗时 = 36 ms

同样 1 万条 SET,3914ms → 36ms,约 108 倍。你机器上数字会不一样(网络/CPU 不同),但 pipeline 必然快一两个数量级——这就是批量操作的性价比。

写完 5MB 大 key 后,用 redis-cli --bigkeys 扫全库(它是用 SCAN 分批扫的,不像 KEYS * 那样卡死线上):

[00.00%] Biggest string found so far "normal:1294" with 10 bytes
[07.19%] Biggest string found so far "bigkey:demo:5mb" with 5242880 bytes

-------- summary -------
Sampled 20001 keys in the keyspace!
Biggest string found "bigkey:demo:5mb" has 5242880 bytes
20001 strings with 5440660 bytes (100.00% of keys, avg size 272.02)

那个 5242880 字节(正好 5MB) 的大 key 被一眼揪出来,而其他 2 万个 key 平均才 272 字节——一个顶两万个。


七、用快了之后的三个病:bigkey、热 key、慢查询

病危害排查/治理
bigkey(值特别大)DEL 时单线程释放内存,整个 Redis 卡死;GET 5MB 占带宽;集群数据倾斜排查:--bigkeys / SCAN 分批扫 + STRLEN 量大小;删除用 UNLINK(异步删,Redis 4.0+)
热 key(一秒被打几万次)全压在一个分片上多副本(hotkey:1~N 随机读)、本地缓存(Caffeine)、拆 key
慢查询单个命令拖慢全局slowlog-log-slower-than 默认 10000 微秒 = 10ms(注意单位是微秒!);SLOWLOG GET n 看记录

八、挑战题(答案都在仓库里,跑起来才知道)

  1. ⭐ 把代码里 TOTAL 改成 50000 再跑:pipeline 和普通循环的倍数差会不会更大?(提示:命令越多,省下的 RTT 越多)
  2. ⭐⭐ 把 slowlog-log-slower-than 改成 1(微秒),随便执行几条命令,再 SLOWLOG GET 5——是不是每条都被记下来了?
  3. ⭐⭐ 用 UNLINK 而不是 DEL 删那个 5MB 的 bigkey,想想为什么线上推荐 UNLINK。
  4. ⭐⭐⭐ 往一个 Hash 里塞 100 万个 field,--bigkeys 看它怎么被揪出来,再试试用 HSCAN 分批删 field。

九、面试回答模板(背下来)

面试官:Redis 为什么快?

三板斧:① 纯内存操作(比磁盘快 10 万倍);② IO 多路复用(epoll 事件驱动,只通知"有事的连接",一个线程盯上万个连接);③ 单线程模型(无锁、无上下文切换)。6.0 的多线程只处理网络读写,命令执行仍单线程。

追问:pipeline 呢?

把多条命令攒成一批一次性发送,省的是网络 RTT 不是执行时间;适合批量无依赖的命令,命令之间有依赖就不能用。

追问:bigkey 有什么危害?

DEL 阻塞全局、占带宽、集群数据倾斜;排查用 --bigkeys/SCAN,删除用 UNLINK。


十、总结

三板斧一句话面试必背
纯内存读写改字节,纳秒级必要条件不是充分条件
IO 多路复用epoll 事件驱动,谁有事找谁银行柜台/前台经理类比
单线程无锁、无上下文切换6.0 多线程只碰网络 IO

记住这条:Redis 快 = 内存 + 事件驱动 + 单线程;批量命令用 pipeline(实测 108 倍);大 key 用 UNLINK 删、用 --bigkeys 查。


关于这个系列

本文是「Java 后端实战精通营」系列第 5 篇,原则:实战驱动、由浅到深、面试向,每篇文章的结论都可以亲手验证。

👉 Redis 实战精通营(10 课):gitee.com/j67mk2/redi…

  • 本文对应源码位置:lesson-08/(pipeline 对比 + 造 bigkey,挑战题 1 的答案就在 Main.java 里)
  • 系列已发布:《分布式锁》《缓存一致性》《事务与 Lua》《Redis 高可用》——从"怎么用"到"怎么快",Redis 主线快收尾了
  • 后续系列(陆续发布):Dubbo / JVM / JUC / RocketMQ / MySQL / Elasticsearch

下一篇预告:《Redis 企业级场景实战:排行榜、限流、布隆过滤器,一次写全》——ZSet 排行榜、Lua 限流、手写布隆过滤器,全是面试和生产天天用的东西。

跑完上面任何一步遇到问题,把终端输出发评论区,一起排查。