Redis 八股文深度解析:线程模型、内存管理与事务机制全掌握

发布时间:2026/8/30 6:54:08
Redis 八股文深度解析:线程模型、内存管理与事务机制全掌握 Redis 八股文这个话题在面试圈里几乎跟“Java 集合”“MySQL 索引”一个级别属于背了能过、不背就凉的高频区。不过我自己面过不少候选人也当过被面的那一方一个很深的感受是把八股背熟的人很多但能把“为什么”讲清楚的人很少。比如问 Redis 为什么快很多人会条件反射式地回一句“因为单线程、基于内存”然后就没有然后了。这篇我自己整理总结的东西就是想把这些基础问题从“背答案”提升到“理解逻辑”的程度覆盖基础问题、线程模型、内存管理、事务四个大块适合准备面试的开发者也适合日常被 Redis 性能问题折磨、想系统补一遍底层知识的同学。内容会尽量讲清原理、给足实操细节顺手把最容易踩的坑标出来。1. 基础问题先搞清楚 Redis 到底在解决什么问题1.1 Redis 的定位不只是缓存而是一个数据结构服务器Redis 全称是 Remote Dictionary Server直译过来就是“远程字典服务”。官方的定义更准确它是一个基于内存的、支持多种数据结构的 key-value 存储系统。很多人一提到 Redis 就默认它是缓存这个概念没有错但格局小了。Redis 被创造出来的初衷是解决当时实时统计场景下的性能问题后来因为数据结构丰富、操作原子、响应极快才逐渐在缓存、分布式锁、排行榜、延迟队列、限流、分布式会话等场景全面开花。从数据结构上看Redis 并不只是简单的 key-value 映射。它支持 String、List、Hash、Set、ZSet还有 Bitmap、HyperLogLog、Geo、Stream 等扩展类型。这意味着你可以在服务端直接完成很多业务操作而不需要把数据拉到应用层再算。举个例子排行榜功能如果在业务代码里做需要先查全量数据、排序、再去重量大时性能很难看但 Redis 的 ZSet 天生就支持按分数排序和范围查询一个 ZADD 加一个 ZREVRANGE 就能拿到 Top N省时省力。从使用场景上看Redis 最常见的角色仍然是缓存这个定位要说清楚。缓存面对的核心矛盾是数据库的读写性能有天花板而高并发场景下大量请求同时落到数据库会直接把连接池和磁盘 IO 打挂。Redis 作为一层廉价的高速缓存层把热点数据提前放进去请求先打 Redis没命中再落到数据库能极大降低数据库压力。但缓存有缓存的坑比如缓存穿透、击穿、雪崩这属于另一篇总结的范畴这里先不展开。1.2 为什么 Redis 这么快一份面试官想听的完整答案“Redis 为什么快”是基础问题里的超高频考点。初级答案就是“基于内存、单线程”这个答案只能算及格没办法拿高分。我复习的时候喜欢把整条链路拆开从存储介质、执行模型、网络模型、数据结构四个层面来讲这样逻辑完整面试官追问的时候也不会慌。首先是存储介质。Redis 的数据主要存在内存里内存的访问延迟是纳秒到微秒级别而磁盘的随机读写延迟是毫秒级别两者差了三到四个数量级。这是 Redis 快的根本前提也是最直白的原因。但要注意现在 Redis 也有持久化数据会落盘但落盘是异步的、批量写的不阻塞主流程所以日常读写仍然以内存为主。其次是执行模型。Redis 核心命令的执行是单线程串行的这意味着没有锁竞争、没有线程切换开销、没有并发写冲突。多线程程序在高并发下光锁和上下文切换就能吃掉很大的性能单线程模型天然规避了这些问题每个命令都是原子性的不需要额外加锁。这也是后来 Redis 6.0 引入多线程 IO 时特意保留“命令执行仍然单线程”的原因。第三是网络模型。Redis 使用基于 IO 多路复用的事件驱动模型在 Linux 上用的是 epoll配合非阻塞 IO 和事件循环可以在一个线程里同时管理成千上万个客户端连接。这个部分在后面的线程模型章节会详细拆这里先记住一个关键词IO 多路复用。第四是数据结构。Redis 表层的五种数据类型底层都有精细的编码方案。比如 String 用的 SDS简单动态字符串解决了 C 字符串 O(n) 拼接和二进制安全问题ZSet 的跳表能在 O(log n) 内完成范围查询Hash 在字段少时用紧凑的 listpack字段多时自动升级为哈希表。数据结构上的这些优化让 Redis 单条命令的时间复杂度被压得很低内存利用率也更高。1.3 数据结构不只是五种类型底层编码才是分水岭面试常问“Redis 有哪几种数据类型”这是送分题。但真正拉差距的题是“ZSet 底层为什么用跳表不用红黑树”“Hash 为什么既能用 listpack 又能用哈希表”。我建议直接把底层编码表背熟并且理解每种编码的触发条件。String 的底层编码有三种int、embstr、raw。保存整数时用 int直接二进制存储字符串长度比较短时用 embstr把 redisObject 和 SDS 分配在一块连续内存里减少一次内存分配字符串变长时升级为 raw。这里注意String 类型不能简单地理解成字符串它可以存整数、二进制数据甚至可以存图片或序列化后的对象只要不超过 512MB。List 在 Redis 3.2 之后用 quicklist本质上是双向链表和 ziplist 的合体每个节点是一个压缩列表多个节点串成双向链表。Redis 7.0 之后内部实现进一步替换成 listpack。开发中更关注的是 List 能实现消息队列、最新列表这些场景但底层编码也要知道否则解释不清为什么 List 在元素少、元素小的时候内存那么省。Hash 和 ZSet 类似小数据量用 listpack 节省内存数据量变大后自动升级为哈希表或跳表加哈希表。Set 的整数元素用 intset非整数或数量大时转换成哈希表。ZSet 是跳表加哈希表的组合跳表负责排序和范围操作哈希表负责 O(1) 按 member 查分数。这里有个高频追问为什么 ZSet 不直接用红黑树因为跳表的实现更简单、更容易调试而且做范围查询时比红黑树更方便不需要中序遍历直接沿链表走就行。1.4 Redis 和 Memcached别再答“前者能持久化”就结束了面试里“Redis 和 Memcached 的区别”也是经典题。最肤浅的答案是“Redis 能持久化Memcached 不能”。这个答案没错但只答到了表面。实际对比可以从数据结构、持久化、线程模型、内存管理、集群能力、适用场景几个维度展开。从数据结构上说Memcached 只支持简单的 key-value 字符串数据操作只有 set、get、delete 这几个业务想在服务端做复杂运算根本不现实。Redis 支持 String、List、Hash、Set、ZSet 等丰富类型能做排行榜、计数器、分布式锁这类复杂业务。从持久化看Redis 支持 RDB 和 AOF 两种持久化方案可以按策略把内存数据落盘Memcached 则是纯内存缓存重启即失。从线程模型看Memcached 是多线程处理请求虽然线程多但锁和同步开销也大Redis 是单线程执行命令加多路复用命令串行执行更可控。从内存管理看Memcached 有自己的 Slab Allocator按固定大小分块分配内存Redis 则依赖系统的 jemalloc 等分配器灵活性更高。集群方面Memcached 的分布式主要靠客户端哈希取模Redis 有官方的 Cluster 模式和主从复制方案运维能力更强。对比下来可以总结成Memcached 更像一个“纯缓存引擎”适合缓存小体积的简单数据Redis 更像一个“内存数据服务”既能当缓存也能承担存储和计算职能。这种从产品定位出发的回答比单纯罗列区别更容易让面试官眼前一亮。2. 线程模型单线程的真相以及 6.0 后的多线程演进2.1 “单线程”到底指哪一部分“Redis 是单线程的”这句话几十年前确实成立但今天再说就得小心。如果你说的单线程是“Redis 整个进程只有一个线程”那是不准确的。Redis 的主线程确实只有一个负责接收命令、执行命令、处理事件循环但 Redis 进程里还有后台线程在干活比如持久化的时候会有专门的线程负责 RDB 快照或 AOF 重写再有异步删除命令 UNLINK 时大 key 的内存释放也会丢给后台线程慢慢清理。所以更准确的说法是Redis 的“单线程”指的是命令执行引擎是单线程的所有业务命令在同一个线程里串行执行不存在并发修改同一份数据的问题。但 IO 处理、持久化、异步删除这些脏活累活早就已经有专门的线程在背后帮忙了。理解到这一层才不会在面试里被“Redis 6.0 引入了多线程所以单线程是谣言”这种话术带偏。面试追问的另一个角度是为什么命令执行必须保持单线程。我的理解是Redis 的核心操作大多是内存读写延迟本身已经极低真正耗时的瓶颈往往在网络 IO 和上下文切换上。如果把命令执行改成多线程就要引入锁、同步、线程调度这些开销可能比省下的那点时间还多还会增加实现复杂度和出 bug 的风险。倒不如保持单线程串行换取极致的简单、可预测和零锁竞争。2.2 IO 多路复用和 Redis 事件循环的关系很多讲 Redis 线程模型的文章会直接抛出一句“Redis 用了 IO 多路复用”但没说明白它解决了什么问题。这里我用比较通俗的方式解释如果服务器没有 IO 多路复用每来一个客户端连接就需要开一个线程专门伺候它连接多了线程数暴涨系统在上下文切换上把自己累死。而 epoll 这类机制允许一个线程同时监听成千上万个连接上的读写事件哪个连接有数据来了我就去处理哪个没有数据的事件就继续挂着等。这就是“多路复用”含义一个线程复用服务多路连接。Redis 的事件循环主要由文件事件和时间事件两部分组成。文件事件指的就是 socket 连接上的读写事件时间事件负责处理周期任务比如定期删除过期 key。整个过程大体是先去 epoll 等待可读或可写事件等到底层告知有事件 ready 了Redis 再回调对应的事件处理器把命令读取出来、执行、把结果写回客户端。因为这个模型把阻塞等待和事件处理分开了所以单个主线程就能管理几万个连接配合内存高速读写压测时单实例 QPS 能到十万级别。为了加深理解可以记住一个结论Redis 的快其实是“网络事件驱动 内存存储 单线程执行”三者相互配合的结果。线程模型只是其中一块拼图缺少内存存储的高速度单线程并发优化再怎么做也有天花板。2.3 Redis 6.0 引入多线程后为什么没有打破“单线程铁律”Redis 6.0 最大的变化之一就是引入多线程 IO。这里要先明确一个前提Redis 6.0 的多线程不是用来执行命令的而是用来处理网络 IO 的。实际场景中如果每条命令都很简单整体瓶颈常常出在 socket 读写和协议解析上而不是命令执行本身。这个时候把网络读写交给多个 IO 线程并行处理就能显著提升吞吐量。具体的工作方式是这样的主线程仍然负责接收新连接、执行命令、维护全局状态当一批客户端请求到达时主线程会把读 socket、解析协议这类任务分发给多个 IO 线程并行处理命令解析完成后主线程拿来逐条执行最后再把响应结果的写回操作分发给 IO 线程去做。也就是说不论 IO 怎么并行命令真正在内存里的执行顺序仍然是单线程串行的依旧不存在锁和并发写问题。默认情况下Redis 的 io-threads 是关闭的需要手动配置才能启用。为什么默认关因为多线程 IO 带来的提升不是绝对的当命令本身很复杂、耗CPU时IO 线程加速的收益会被命令执行时间稀释反而可能因为线程切换增加开销。合理做法是先压测如果确认瓶颈在网络解析再通过配置文件或 CONFIG SET 打开比如设置 io-threads 4并且要注意 io-threads 的关闭状态和开启状态不能随意热切换最好在启动时就确定。2.4 线程模型里的经典追问线程模型这块比较常被追问的还有两个问题。第一个是“Redis 单线程能充分利用多核 CPU 吗”答案是不能但这不算缺陷。一个 Redis 实例只能用一个核心来执行命令所以当 CPU 成为瓶颈时合理的解法是部署多个 Redis 实例通过集群模式把请求分散到不同核心甚至不同机器上而不是寄希望于一个进程把整台机器的 CPU 全吃满。第二个是“既然命令执行单线程为什么偶发大量阻塞时整个 Redis 都动弹不得”。这个问题天然成立因为单线程的最大弱点就是怕长耗时操作。比如执行 KEYS 命令匹配大键、删除超大 key、执行一段复杂的 Lua 脚本都会卡住主线程后面所有请求都排队等待。这也是为什么线上规范里总说生产环境禁用 KEYS、大 key 不要直接 DEL而要用 SCAN 或 UNLINK。理解了这个短板才算真正把线程模型学懂而不是只记住“单线程快”。3. 内存管理从过期清理到淘汰策略再到碎片治理3.1 maxmemory不给 Redis 设定上限等于埋雷Redis 是内存数据库如果放任不管数据量增长到物理内存上限操作系统就该触发 OOM 或者开始疯狂 swap整台机器都会卡死。所以生产环境第一步就是给 Redis 设置合理的内存上限。可以通过配置文件 maxmemory 设置也可以在运行时用 CONFIG SET maxmemory 修改。单位是字节比如 maxmemory 4gb 表示上限 4GB。设置上限之前得想清楚物理内存总量是多少系统还有没有其他进程在占用内存Redis 所在机器是否还需要预留一部分内存给操作系统和 swap。一般建议 Redis 的 maxmemory 不要超过物理内存的 70% 到 80%并且要留出缓冲给 AOF 重写、内存碎片、连接缓冲等额外消耗。如果一台机器上同时跑着 Redis 和 MySQL那更要精打细算不然 Redis 这边数据一多可能拖垮整个数据库服务。当 Redis 的内存达到 maxmemory 限制后如果你配置了淘汰策略它就会按照策略开始淘汰 key如果配置的是 noeviction那么所有会占用内存的写命令都会直接返回 OOM error。很多初学者一上来会踩的坑是明明设置了 maxmemory 却没设淘汰策略结果线上 Redis 突然拒绝写入业务瞬间报错。这个排查思路后续会提到先说结论如果不希望 Redis 因为内存满而拒绝服务应该明确配置合适的淘汰策略。3.2 过期删除是惰性加定期不是定时全扫Redis 对带 TTL 的 key 采用两种过期删除策略一种是惰性删除一种是定期删除。惰性删除的意思很简单当客户端访问一个 key 时Redis 会先检查它是否已经过期如果过期就立刻删除并返回空否则正常返回数据。这种策略的优点是省资源只有被访问的过期 key 才会被处理缺点也很明显如果很多过期 key 一直没被访问它们就会一直躺在内存里占用空间。所以需要用定期删除来兜底。Redis 默认每 100 毫秒执行一次过期 key 清理每次不是扫全库而是从设置了过期时间的 key 里随机抽一批检查并删除其中的过期 key。如果这一批里过期 key 的比例超过一定阈值就说明清理压力大会重复执行多次。这个设计思路和大多数服务端的“采样清理”一样不追求某个时刻把过期 key 全部清干净而是通过频率和随机的组合把过期 key 对内存的影响控制在可接受范围内。定期删除的代价是它可能清理不干净。假如大量 key 同时过期且一直没被访问定期删除的抽样也可能漏掉很多内存压力就会短暂升高。这时候就需要内存淘汰策略来做最后的防线。注意过期删除和内存淘汰是两个不同层面的概念前者是“这个 key 已经到时间了该删”后者是“内存不够了需要赶走一些 key”两者不能混为一谈这也是面试里容易踩的坑。3.3 八种内存淘汰策略怎么选Redis 在 4.0 之后提供了 8 种淘汰策略命名上分成了 allkeys 和 volatile 两大阵营。allkeys 开头的策略淘汰范围是所有 key不管有没有设置过期时间volatile 开头的策略淘汰范围只限于设置了过期时间的 key如果这些 key 都清空了内存还是不够就更极端地报错。具体来说有 noeviction、allkeys-lru、allkeys-lfu、allkeys-random、volatile-lru、volatile-lfu、volatile-random、volatile-ttl。选择策略不是凭感觉要看业务对数据丢失的容忍度。如果 Redis 只做缓存丢了可以从数据库回源建议用 allkeys-lru 或 allkeys-lfu让 Redis 优先淘汰最久没访问或访问频率最低的 key。如果 Redis 还承担了临时数据存储的职责比如保存验证码、限流计数器这些 key 本身有 TTL用 volatile-lru 或 volatile-ttl 更合适。如果 Redis 里存的是不能丢的强一致数据那就用 noeviction宁可让它报错也不能默默淘汰数据。LRU 和 LFU 的区别也值得单独说。LRU 是 Least Recently Used淘汰一段时间内最久没有使用的 key实现简单但存在扫描型访问污染缓存的问题LFU 是 Least Frequently Used额外统计访问频次能更好地保留高频热点但它的访问计数本身也要消耗一点内存。Redis 的 LRU 实现并不是标准的双向链表式 LRU而是近似 LRU通过采样加淘汰池来模拟真实 LRU 行为具体由 maxmemory-samples 参数控制采样数量默认 5适当调大会更接近真实 LRU但会消耗更多 CPU。3.4 内存碎片和 bigkey线上排查的常见坑内存碎片是 Redis 内存管理中很容易被忽视的问题。碎片产生的原因主要有两个一是内存分配器的分配和释放策略容易在频繁变动的数据量上留下无法利用的小空隙二是频繁删除和修改 key比如反复对大 key 进行 append 或重写内存重新分配时就会形成碎片。查看方式是用 INFO memory 命令关注 mem_fragmentation_ratio 这个指标它表示物理内存使用量和 Redis 实际申请内存的比值。如果这个值持续高于 1.5说明碎片比较严重白白浪费了不少内存。处理碎片主要有两个思路。第一个是开启自动碎片整理Redis 配置项 activedefrag 设为 yes再配合 active-defrag-ignore-bytes、active-defrag-threshold-lower、active-defrag-threshold-upper 这几个参数让 Redis 在后台把分散的内存块挪动合并。注意碎片整理会占用 CPU 和内存一般建议在业务低峰期开启并且设置好触发阈值。第二个思路是业务层面优化尽量少用 replace 这类频繁修改 key 的操作删除大 key 时用 UNLINK避免主线程因释放大内存而卡顿。bigkey 的问题和内存碎片经常绑在一起出现。bigkey 并不是某种数据类型独有而是指单个 key 存储的数据量非常大比如一个 List 里有几百万个元素一个 String 有几十 MB。bigkey 的危害有三个一是读取和删除时都容易阻塞主线程二是网络传输会吃掉大量带宽三是容易导致 Redis 内存不均衡在集群模式下某些节点可能被拖垮。排查方式可以用 redis-cli --bigkeys 命令也可以用 SCAN 配合 DEBUG OBJECT 或 MEMORY USAGE 自己造一个扫描脚本。定位到 bigkey 后常见的治理策略是拆分、压缩和设置合理的过期时间能换数据结构就换能少存就别硬存。4. 事务Redis 的事务和 MySQL 事务从底层就不是一回事4.1 基本用法MULTI、EXEC、DISCARD、WATCH先看最简单的用法。Redis 事务从 MULTI 开始后面跟着一组命令命令不会立即执行而是被放进一个队列直到收到 EXEC 才批量执行。如果中途想反悔可以发送 DISCARD 丢弃队列里的所有命令。举个例子127.0.0.1:6379 MULTI OK 127.0.0.1:6379 SET name redis QUEUED 127.0.0.1:6379 INCR counter QUEUED 127.0.0.1:6379 EXEC 1) OK 2) (integer) 1从输出可以看到每条命令在 MULTI 之后不会直接返回结果而是返回 QUEUED。只有 EXEC 时才会真正执行并按顺序返回每个命令的结果。这里需要特别注意的是Redis 事务的“批处理”和“原子性”不能画等号。它确实保证队列里的命令会连续执行中间不会被其他客户端的命令插入但命令在执行过程中如果发生运行时错误Redis 并不会回滚已经执行成功的命令这一点和 MySQL 事务差异巨大。WATCH 命令提供的是乐观锁能力后面专门开一节详细说。这里先把完整的四个命令记在脑子里MULTI 开启事务EXEC 提交执行DISCARD 放弃事务WATCH 监控键。还有一个 UNWATCH 可以取消监控不过日常用得少。4.2 原子性、一致性、隔离性、持久性逐个拆面试里只要提到“Redis 事务”很容易被追问“Redis 事务满足 ACID 吗”。这个问题如果用一句话回答可能被反杀最好分开分析。首先是原子性。严格意义上说Redis 事务并不满足原子性。原子性的要求是“要么全部成功要么全部失败”而 Redis 事务遇到运行时错误比如对一个 String 类型的 key 执行 LPUSH会跳过当前命令继续执行后面的命令。换句话说部分成功部分失败的情况是真实存在的。Redis 官方文档也明确说过不支持回滚。所以“Redis 事务是原子的”这个说法只能在“执行过程不被其他命令打断”这个意义上成立也就是所谓的“隔离性”保证了原子排他而不是错误回滚。其次是一致性。Redis 单线程执行命令在绝大多数情况下事务不会把数据结构搞坏因为命令要么合法要么非法非法命令会被拒绝。但事务过程中如果出现编程错误比如修改了错误的数据类型虽然 Redis 会报错但已经执行的命令效果仍然保留可能导致业务逻辑上的数据不一致。所以一致性只能称得上“基本满足但有条件”。再次是隔离性。这是 Redis 事务做得最好的部分。因为命令执行是单线程串行的事务内命令排队后EXEC 时会一口气执行完中间不可能插入其他客户端的命令天然具备串行化隔离。不需要 MVCC也不需要锁。最后是持久性。Redis 事务本身不保证持久性落盘与否取决于持久化配置。如果没开启 AOF或者 AOF 采用 everysec 策略事务提交后如果 Redis 立刻宕机数据很可能丢。要保证每条事务命令都落盘得用 appendfsync always但代价是性能显著下降需要根据业务容忍度权衡。4.3 为什么 Redis 坚持不回滚面试里经常有人问“为什么 Redis 事务不支持回滚”。这个问题我也疑惑过很久后来看了官方文档才想明白。官方解释有两条一是 Redis 事务中的错误通常是编程错误如果要回滚说明开发者的命令没写对二是回滚会引入事务执行前的状态保存、状态恢复等复杂机制这跟 Redis 追求简单高效的设计哲学相悖。从工程角度我自己的理解是回滚不是免费午餐它需要记录每一步操作之前的旧值遇到错误时再逐条恢复。在 MySQL 里undo log 是事务模块的核心组件写日志、维护版本链都有显著开销。Redis 本身就是单线程高速执行模型如果每次事务都要做回滚准备性能肯定会被拖累。既然 Redis 主打的场景是缓存和实时计算业务上通常可以容忍少量逻辑不一致那采用“出错了继续执行”的策略反而让实现更轻量。所以答这道题时不要只说“Redis 不回滚是因为作者懒”而是从设计哲学和性能成本两个维度回答展现出你理解 Redis 做取舍的原因。如果你还能补充一句“正因为不支持回滚写 Redis 事务时要格外注意命令的顺序和类型的正确性”面试官会觉得你不仅有知识还有实战意识。4.4 WATCH 乐观锁的完整流程与重试策略WATCH 是 Redis 事务实现“乐观锁”的机制适合处理“先检查再修改”的并发场景。举个库存扣减的例子假设我们要扣减某个 key 的值先读当前值判断是否大于 0再执行扣减。如果两个请求同时读到同一个值就会出现超卖。用 WATCH 可以解决这个问题流程是这样WATCH stock GET stock MULTI DECR stock EXEC如果执行 EXEC 的时候Redis 发现被 WATCH 的 stock 在 WATCH 之后被其他客户端修改过这次事务就会失败EXEC 返回 nil。这意味着我们的扣减没有执行需要业务层重试重新 WATCH、重新读值、重新判断、重新尝试。这是典型的乐观锁模式不加锁而是在提交时检查版本是否变化冲突了就重试。为什么 Redis 用乐观锁而不是悲观锁核心原因是 Redis 单线程天然没有死锁问题悲观锁的“先锁定再操作”反而会增加复杂度和传输开销。乐观锁只在提交瞬间做一次比较和原子执行对简单内存操作来说足够快。不过要注意WATCH 的成功与否取决于事务执行前是否有其他客户端修改被监控的 key所以在高并发写场景下重试频率可能很高业务层要设置合理的重试次数上限避免无线循环。在代码里重试逻辑可以这样写伪代码while True: redis.watch(stock) current redis.get(stock) if current is None or int(current) 0: redis.unwatch() raise Exception(库存不足) pipe redis.pipeline() pipe.multi() pipe.decr(stock) result pipe.execute() if result is not None: break这段逻辑的关键是WATCH 要在 MULTI 之前发起EXEC 失败时说明并发冲突继续循环重试成功则跳出。4.5 事务、管道、Lua 脚本三者的边界是什么很多人会把 Redis 的事务、管道Pipeline、Lua 脚本混在一起因为它们看起来都是“把一堆命令一次性发给 Redis”。其实这三者的目的和能力差别很大。管道的主要目的是减少网络往返把多个命令打包发送给 Redis然后一次拿回所有结果。它不保证命令一起执行也不保证不被其他客户端命令插入更不支持回滚。比如你在 pipeline 里发 100 条 setRedis 会一条条按顺序执行中间其他客户端的命令可以穿插进来。管道适合批量读写的场景关注的只是“网络开销变低”而不是“原子性”。Redis 事务保证的是命令在 EXEC 时被打包执行中间不会被其他客户端命令插入但前面已经说了它也没有回滚和复杂逻辑能力。如果要按条件执行命令、循环执行命令或者把多个命令封装成一个脚本整体执行那就该用 Lua 脚本。Redis 从 2.6 开始支持内嵌 Lua用 EVAL 或 EVALSHA 执行脚本。因为脚本在 Redis 服务端执行且执行期间主线程不会处理其他命令所以 Lua 脚本天然具备原子性还能写 if、for 等逻辑。我的建议是纯批量命令、不要求逻辑判断的用管道要求多条命令连续执行且不能被插入、不需要业务逻辑的用事务需要复杂逻辑且必须保证原子性的用 Lua 脚本。另外补一句Redis 7.0 后推出了 Redis Functions可以把 Lua 脚本像存储过程一样注册管理比直接拼 EVAL 字符串更规范适合在生产里使用。4.6 面试进阶Redis 事务和分布式事务别混为一谈聊到事务很容易话题滑向分布式事务。这里要强调一个边界Redis 事务是单节点、单机范围内的批处理机制它不解决跨多资源的一致性。真正的分布式事务比如跨数据库、跨 Redis 节点、跨微服务的事务需要 TCC、Saga、消息事务、最大努力通知等方案依赖的是事务协调器、消息中间件和幂等设计而不是 Redis 原生的 MULTI/EXEC。面试里如果被问到“Redis 能不能做分布式锁”那是另一个话题和事务不是一回事。分布式锁要的是互斥和防误删通常用 SET NX 加过期时间实现。想要高可用还要引入 RedLock 或 Redisson 的看门狗续期机制。这些主题已经能单独开一篇八股总结了感兴趣的可以等后续的总结二。5. 把八股变成面试加分项常见误区和速查表5.1 背了那么多还是会答错的几个点总结了这么多我印象里最容易被背错、也最容易被面试官挖坑的主要有这么几点。第一Redis 事务不支持回滚。很多人一听事务就默认“出错回滚”这是错的。Redis 事务在运行时报错时前面的正常命令依然执行成功。回答这一题时最好主动区分“入队错误”和“执行错误”在 MULTI 后如果一条命令语法错误整个事务连 EXEC 都会被拒绝执行如果语法没问题、执行时才发现类型不对那这个错误命令之前的命令会正常生效。这个细节知道的人少说出来就是加分项。第二内存淘汰策略不等于过期删除策略。内存淘汰是“内存满了我的地盘我做主踢一些 key 出去”过期删除是“这个 key 到期了我主动清理”。两者都会删除 key但触发条件完全不同。生产环境里经常有同学以为设置了 expire 就万事大吉结果内存还是被一大堆没被访问的过期 key 撑爆就是因为没理解定期删除的抽样特性也没设置淘汰策略兜底。第三Redis 6.0 多线程不改变命令执行模型。聊到多线程 IO有些候选人会兴奋地说“Redis 6.0 支持多线程了所以单线程模型过时了”。这个说法是片面的。多线程 IO 解决的是网络读写瓶颈命令执行仍然单线程。真正主推这个特性的是官方在 IO 密集场景下的性能优化不是让 Redis 变成并发编程框架。第四持久化不是 Redis 的默认安全网。没有配置 AOF 或 RDBRedis 宕机重启后内存数据就没了这是正常现象。很多人把 Redis 当数据库用却忘了配置持久化然后出了问题甩锅给 Redis。实际上自己打开配置文件看五分钟就能避免这个事故。5.2 高频问题速查表按我自己的习惯整理八股时会把“问题、一句话答案、深挖点”存在一张表里考前扫一遍比翻书效率高得多。下面这张表覆盖了本文四个板块的核心题可以直接抄走。问题一句话答案深挖点Redis 为什么快内存存储 单线程执行 IO 多路复用 高效数据结构从四层展开避免只说“内存和单线程”单线程为什么还能高并发IO 多路复用让一个线程管理海量连接epoll 的事件驱动模型Redis 6.0 多线程用来干什么处理网络 IO不执行命令命令执行仍是单线程、无锁Redis 的过期删除策略惰性删除 定期删除定期删除是抽样不是全扫内存淘汰策略有哪些8 种分 allkeys 和 volatile 两类按业务容忍度选择如何排查 bigkeyredis-cli --bigkeys、MEMORY USAGE用 SCAN 避免阻塞Redis 事务满足 ACID 吗隔离性最好原子性、持久性不满足为什么不支持回滚WATCH 是怎么工作的乐观锁EXEC 时发现键被改则失败重试策略怎么写Pipeline、事务、Lua 区别管道优化 RTT事务保证连续执行Lua 保证原子加逻辑按场景选型有了这张表临考前看一眼基本不怕基础问答题。但光靠速查表是不够的题目一旦变成“线上 Redis 内存持续上涨你怎么排查”就需要把本文里的内存管理、过期删除、淘汰策略、bigkey 排查全部串起来。5.3 我复习 Redis 八股的一点心得说点个人经验。最初我也喜欢把面经一条条复制到笔记里背得滚瓜烂熟但面试官只要稍微换个角度问“为什么”我就卡壳。后来我换了一种复习方法每一个结论都去反问一句“为什么要这样设计”。Redis 为什么不支持回滚为什么要用跳表为什么要用 IO 多路复用为什么事务里出错了不停止。把这些问题想通之后那些“八股句子”就不再是一串需要记忆的文字而是可以现场推导出来的知识点。另一个小技巧是画图。线程模型、过期删除、WATCH 乐观锁这种流程类内容用一张简单的时序图或者流程图把步骤标出来记忆会牢靠很多。不看文档试着把 Redis 一次完整事务从 MULTI 到 EXEC 的时间线画出来再解释每一步为什么那么处理比重复背十遍文字都管用。Redis 基础八股还有不少内容像持久化、集群、哨兵、缓存三大问题、分布式锁这些我在下一篇总结里继续展开先把这篇里的内容吃透后面再聊会轻松很多。