Redis面试八股文16卷:从数据结构到高可用全解析 最近准备面试的朋友十个里有八个会来问 Redis 的八股文到底怎么背。说实话Redis 在面试中的出现频率几乎和 Java 集合、JVM 并列属于不准备就肯定吃亏的考点。很多人能背出五种数据结构、能说出 RDB 和 AOF 的区别但一到追问“为什么”“怎么办”就卡住。这篇《面试八股文》之 Redis 16 卷我按高频考点拆成 16 个问题每一卷都尽量把原理、场景和实际坑位讲透。适合正在准备后端口试、运维面试的同学也适合已经在用 Redis 但想把底层补扎实的开发者。1. 卷一至卷四数据结构与底层编码——五种基础类型背后的设计逻辑面试官问“Redis 为什么快”除了单线程和 IO 多路复用底层的编码设计是一个绕不开的点。Redis 表面上是 String、Hash、List、Set、ZSet 五种数据类型但底层会根据数据量大小和元素特征自动切换编码。不理解这一点很容易被追问到“为什么 String 不能乱存大对象”“Hash 什么时候会内存暴涨”这类问题。1.1 卷一 StringSDS、int 与 embstr 的取舍String 是最常用也最容易答浅的类型。如果你回答“底层就是 C 字符串”大概率会被追问 SDS。SDS 是 Redis 自己实现的动态字符串结构里保存了长度和已分配空间所以获取字符串长度是 O(1)而 C 字符串需要遍历。更重要的是SDS 在拼接时会提前检查空间不会出现缓冲区溢出这种经典安全问题。在 RedisObject 中String 有三种编码int、embstr、raw。当 value 是整数且能用 long 表示时直接用 int 编码不分配额外的字符串内存。当 value 是字符串且长度不超过 44 字节时用 embstrRedisObject 和 SDS 会连续分配一次内存分配搞定超过 44 字节后变成 raw需要两次分配。这个 44 字节不是乱拍的是为了把对象头、SDS 头和数据部分尽量塞进 64 字节的内存分配单元里减少碎片提升缓存命中率。实际项目中我看到很多人喜欢把整个对象序列化成 JSON 后塞进 String。如果对象很大这就会引入 bigkey 风险——读取和网络传输都会变慢甚至阻塞主线程。更好的做法是拆成 Hash 存储或者先做一次压缩再缓存。另外SET key value EX 10和SETEX key 10 value效果一样但 SET 加 EX 参数是原子操作能避免分开执行时中途宕机导致的“永久 key”。1.2 卷二 Hashziplist 与 hashtable 的转换阈值Hash 适合存储对象字段比如用户信息、商品信息。它的底层有两种编码ziplist 和 hashtable。默认情况下当字段数少于 512 且每个字段和值的长度都小于 64 字节时Redis 用 ziplist一旦超过这个阈值就转为 hashtable。为什么小数据量不用 hashtable因为 hashtable 是散列表会有哈希冲突、rehash 等问题而且每个键值对都要单独分配节点内存碎片多。ziplist 是一块连续内存所有元素紧凑排列查询时顺序遍历。数据量小时顺序遍历的耗时甚至可以忽略不计但内存局部性好CPU 缓存命中率高所以反而更快。代价是插入和删除可能触发内存重新分配这就是 Redis 限制它只能用于小数据量的原因。生产环境有个容易踩的坑Hash 的某个字段 value 一旦超过 64 字节整个 Hash 会从 ziplist 升级成 hashtable而且这个升级是不可逆的。如果早期写入时数据很小后来业务开始往某个字段塞大文本Hash 的内存可能瞬间涨不少。监控指标里要留意object encoding key的变化如果发现大 key可以通过拆分字段或者换用 String 存储来治理。1.3 卷三 List从 linkedlist 到 quicklistList 的底层演变是一个很好的“讲故事”素材。Redis 3.2 之前List 根据元素数量和大小在 ziplist 和 linkedlist 之间切换。ziplist 内存省但插入删除要搬移数据linkedlist 插入删除灵活但每个节点要维护前后指针内存开销大。3.2 之后Redis 统一用 quicklist 替代了这两个编码。quicklist 本质是一个双向链表但链表的每个节点不是一个元素而是一个 ziplist。这样一来每个 ziplist 内部可以承载一批元素既保留了链表的插入删除灵活性又通过 ziplist 的紧凑存储降低了指针开销。面试时可以补充一句quicklist 还支持从链表中间插入底层会对 ziplist 做拆分或合并。List 的经典应用是消息队列和最新列表。比如LPUSH加BRPOP实现一个阻塞队列命令可以这么写LPUSH task_queue task_001 BRPOP task_queue 0但要注意原生 List 做消息队列并不保证消息不丢。消费者从 List 中弹出消息后如果处理过程中宕机消息就没了。如果业务对可靠性有要求应该使用 Redis Stream或者干脆交给专业消息队列。另一个常见技巧是LTRIM比如只保留最近 100 条动态LPUSH user_timeline 101 LTRIM user_timeline 0 99这样能避免 List 无限增长也算一种低成本的内存治理。1.4 卷四 Set 与 ZSetintset、跳表与哈希表的配合Set 底层在元素都是整数且数量较少时用 intset 存放当元素不再全是整数或数量超过阈值时转为 hashtable。intset 是一个有序的整数数组查找用二分内存非常紧凑。这里可以引出一个小知识点为什么 Set 的成员是无序的但 intset 内部却有序因为 intset 的有序只是为了二分查找不是给外部提供顺序语义。ZSet 的底层更有意思由 skiplist dict 组合实现。skiplist 负责按 score 排序和范围查询dict 负责按 member 精确查找 score。这样ZSCORE是 O(1)ZRANGEBYSCORE是 O(logN)两种操作都能兼顾。面试常问“为什么用跳表不用红黑树”。我的理解是Redis 不需要红黑树那么复杂的平衡调整跳表实现简单、调试容易并且支持范围查询时不需要像树一样做中序遍历。再配合上 dictZSet 可以达到“既能按成员查分数又能按分数查范围”的效果。实际项目里 ZSet 的典型场景是排行榜。这里有一个严重的坑score 是双精度浮点数不要拿订单号、用户 ID 这类大整数当 score精度会丢。我见过有人把交易流水号直接当 score结果多个不同的流水号被当成同一个值排行榜顺序直接乱掉。正确做法是 score 用时间戳、分数这类语义明确的数据member 用业务 ID。2. 卷五至卷八持久化三板斧——RDB、AOF、混合持久化的真实选择持久化部分面试官最喜欢问“Redis 宕机会丢多少数据”“RDB 和 AOF 怎么选”。这不仅是八股文也是生产环境必须做的设计决策。理解持久化机制核心是理解 fork、写时复制、fsync 这几个概念。2.1 卷五 RDBfork 与写时复制RDB 是 Redis 的内存快照默认生成dump.rdb文件。执行BGSAVE时Redis 会fork()一个子进程子进程负责把内存数据写入临时文件父进程继续处理客户端请求。这里的关键是写时复制Copy On Writefork 之后子进程与父进程共享同一份物理内存页只有父进程修改某个内存页时才会把该页复制一份再在副本上修改。所以 RDB 生成期间父进程的新写入不会丢但也不会被写进这一份快照快照反映的是 fork 时刻的内存状态。面试追问“RDB 会阻塞主进程吗”时可以回答fork 瞬间要拷贝页表大数据量时可能有毫秒级阻塞但后续文件写入由子进程完成不会阻塞。生产配置上有几个点要特别关注。save参数不要配得太激进例如save 900 1表示 900 秒内有 1 次修改就触发BGSAVE如果同时存在多个 save 条件任一满足都会触发。还有stop-writes-on-bgsave-error yes默认开启时一旦磁盘错误导致 BGSAVE 失败Redis 会拒绝写入这是保护机制不是故障。很多线上事故就是因为磁盘满Redis 直接变成只读客户端写入报错。2.2 卷六 AOF三种写回策略的取舍AOFAppend Only File记录的是每个写命令本身重启时重放这些命令来恢复数据。AOF 的刷盘策略由appendfsync控制有三个选项always、everysec、no。always每个写命令都执行 fsync 刷盘最安全但性能开销最大。everysec每秒刷一次盘最多丢 1 秒数据性能适中是 Redis 默认配置。no不主动刷盘由操作系统决定什么时候写盘可能丢的数据更多但性能最好。面试时不要把everysec简单说成“每秒保存一次”它保存的是日志缓冲不是全量数据。理解这一点才能回答为什么 AOF 最多丢一秒数据。AOF 文件会持续增长所以 Redis 需要重写。重写不是把文件变小而是 fork 子进程后扫描当前内存状态生成一组能重建这些状态的写命令写入新文件再替换旧文件。触发条件由auto-aof-rewrite-percentage 100和auto-aof-rewrite-min-size 64mb决定意思是 AOF 文件比上次重写后增长了一倍且超过 64MB 时触发。实际坑位不要在 SSD 上为了“安全”盲目开always每个写命令都 fsync 会产生很大的写入放大把性能打掉。大部分业务用everysec足够如果真的要求强一致优先考虑数据库本身而不是依赖缓存层。2.3 卷七 混合持久化重启后到底加载什么Redis 4.0 引入了混合持久化开关是aof-use-rdb-preamble yes。开启后AOF 重写生成的文件不再是一整份纯命令日志而是先写一份 RDB 格式的全量数据再追加重写期间的增量命令。重启恢复时Redis 先加载 RDB 部分再执行 AOF 部分。这样做的好处很明显RDB 部分是二进制快照加载速度快增量命令部分又保证了数据足够新。面试中提到这一点比单纯说“RDB 快、AOF 安全”更能体现你对新版 Redis 的跟进。但要小心混合持久化生成的文件不再是纯文本的 AOF 格式开头是二进制 RDB。如果团队内有人用旧版工具去分析 AOF 文件可能无法解析。升级 Redis 前要确认运维工具是否兼容。2.4 卷八 持久化配置与生产中的数据丢失场景数据丢失是持久化绕不开的话题。下面的总结是从生产角度整理的场景丢失情况缓解手段RDB 间隔内宕机最后一次 BGSAVE 之后的写入丢失缩短 save 间隔或同时开启 AOFAOF everysec 宕机最多丢 1 秒写入接受 1 秒窗口或改 always主从复制未完成主节点宕机未同步到从节点的数据丢失提升复制可靠性监控复制偏移量内存淘汰触发设置了过期时间或达到 maxmemory 后 key 被删除合理设置淘汰策略和过期时间这里说一个实际踩过的坑某次半夜主节点宕机从节点被哨兵提升为新主但检查数据时发现丢了最近几十秒的记录。原因是主节点min-replicas-max-lag配置得太宽松几十秒内没有从节点同步主节点仍然接受写入结果这些写入随主节点一起消失。后来把min-replicas-to-write和min-replicas-max-lag调紧让主节点在从节点同步明显落后时拒绝写入才把风险控制住。3. 卷九至卷十二从主从到集群——高可用架构不只为“备份”Redis 的高可用方案是分布式系统设计题里的常客。单纯会说“主从复制、哨兵、Cluster”还不够面试官更想看你怎么选、怎么排查问题。3.1 卷九 主从复制全量同步与增量同步的边界主从复制的核心是从节点启动后首先尝试增量同步如果条件不满足就升级为全量同步。全量同步流程大概是从节点发送PSYNC命令主节点判断 runid 和 offset 是否对得上对不上就触发BGSAVE同时把后续写命令写入复制缓冲区主节点把 RDB 文件发给从节点从节点加载完成后再执行缓冲区中的命令。增量同步依赖repl_backlog缓冲区。主节点会把写命令持续写入这个环形缓冲区从节点通过 offset 拉取增量数据。如果从节点断开太久offset 对应的数据已经被新写入覆盖主节点只能做全量同步。面试中常见的问题是“repl-backlog-size怎么配”。最简单的估算方法是每秒写入字节数 × 允许从节点最大断连秒数。举例如果主节点高峰期每秒有 2MB 写入希望从节点断连 5 分钟还能增量同步backlog 至少要2MB × 300 600MB。默认值只有 1MB在写入稍微大一点的业务里几乎是一断连就必然全量同步。3.2 卷十 哨兵机制主切换与脑裂问题的本质哨兵是“监视 通知 自动故障转移”的进程通常以奇数节点部署避免平票。主节点故障时哨兵之间要达成共识这个数量叫 quorum。达成共识后哨兵 leader 会客观下线主节点然后从从节点中选举一个新主完成切换。脑裂问题出现在网络分区场景中。比如主节点与哨兵网络隔离但主节点仍然运行客户端仍可写入此时哨兵把另一个从节点提升为新主等旧主恢复后整个集群就出现了两个主节点。更糟的是旧主在隔离期间接受的写入在它重新加入集群后可能被丢弃因为新主没有这些数据。Redis 在配置文件中提供了两个缓解参数min-replicas-to-write和min-replicas-max-lag。含义是主节点只有在至少有 N 个从节点连接且从节点同步延迟不超过 M 秒时才接受写请求。当主节点与从节点失联超过阈值时它会拒绝写入从而减少脑裂期间的脏数据。生产环境建议按要求调小但也要避免误伤正常的写入可用性。3.3 卷十一 Cluster 集群16384 个槽位的重定向逻辑Cluster 模式把数据分成 16384 个哈希槽每个 key 经过CRC16(key) % 16384计算后映射到某个槽位槽位再分配给不同节点。这样扩容、缩容时只需要迁移槽位不用全量重做。客户端请求槽位不匹配时节点会返回一个错误重定向常见两种MOVED和ASK。MOVED表示槽位已经属于另一个节点客户端需要更新本地路由表后续请求直接发给新节点ASK表示槽位正在迁移过程中数据可能还在旧节点也可能已经在新节点客户端需要先发ASKING再访问新节点。面试加分点为什么槽位总数是 16384因为 Cluster 的节点间同步槽位信息用 bitmap16384 个槽位需要 2KB 的 bitmap这在心跳包里体积可控。如果槽位数太多心跳包会变大太少数据分布可能不够均匀。16384 是在数据规模、消息大小、迁移粒度之间取的一个平衡。3.4 卷十二 架构选型单机、主从、哨兵还是 Cluster架构选型是面试里很有区分度的问题。单机 Redis 适合数据量小、可靠性要求不高的场景部署简单但一台机器挂了就全部不可用。主从复制提供了副本冗余但主节点故障时需要手动切换不具备自动恢复能力。哨兵在主从基础上加入自动切换适合数据量能装进单机内存、业务希望快速故障转移的场景。Cluster 适合数据量超过单机内存或者需要横向扩展读写的场景但会引入跨节点操作的复杂度。一个实际建议如果业务数据量远小于单机内存就没有必要上 Cluster。很多团队一开始就上 Cluster结果事务、pipeline 因为跨 slot 限制根本用不了反而增加维护成本。如果决定上 Cluster要提前用 hash tag 把需要原子操作的 key 固定在同一个槽位例如把user:1001:fans写成{user:1001}:fansRedis 只对大括号内的部分做 CRC16。这里放一张简化选型表方案数据容量自动切换运维复杂度适用场景单机单机内存无低开发环境、缓存实验主从单机内存副本无中需要备份可容忍手动切换哨兵单机内存主节点有中大多数中大型业务缓存Cluster可扩展有高数据量超过单机内存4. 卷十三至卷十六缓存三大坑与治理实战这部分是面试中最能拉开差距的地方也是实际项目里最容易出事故的地方。能答出“缓存穿透、击穿、雪崩”已经不算稀奇关键是能说清应对细节和背后的权衡。4.1 卷十三 缓存穿透、击穿、雪崩三种场景的区分与防护先区分三个概念穿透查询一个不存在的 keyRedis 没有数据库也没有。每个请求都直接打到数据库相当于缓存失去了保护。解决办法是布隆过滤器拦截不存在的 key或者把空值也缓存一段时间。击穿某个热点 key 在过期瞬间大量请求同时打到数据库。因为只有一个 key数据库压力瞬时放大。解决办法是互斥锁、逻辑过期、永不过期加异步更新。雪崩大量 key 在同一时间段集中过期或者 Redis 宕机导致大量请求打到数据库。解决办法是过期时间加随机值避免同一时刻大规模失效配合限流降级和集群高可用。我见过不少同学会说“加随机过期时间解决雪崩”但一问“击穿怎么办”就只会说加锁。面试里可以顺带补充一句击穿本质上也是并发问题可以用互斥锁保证只有一个请求去回源其他请求等待结果也可以提前把热点 key 设置为逻辑过期后台任务更新缓存。这样回答会给面试官留下“真的处理过线上问题”的印象。4.2 卷十四 分布式锁SetNX 的坑与 Redisson 的正确用法分布式锁是 Redis 面试中的“保温题”。最经典的实现是用SETNX但初学者最容易犯的错误是把“加锁”和“设置过期时间”拆成两步SETNX lock_key unique_value EXPIRE lock_key 30如果执行到 EXPIRE 之前进程崩溃锁就会永远存在。正确姿势是一条原子命令SET lock_key unique_value NX EX 30释放锁也不是直接DEL因为可能把自己的锁删掉别人的锁。释放时要先用 Lua 脚本比较 value再删除if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end用 Lua 是因为要保证“判断删除”的原子性。value 建议用 UUID 或业务请求 ID确保锁持有者是唯一的。Redisson 把看门狗机制封装好了锁默认 30 秒过期任务没执行完会自动续期避免锁提前释放。但要注意如果应用发生长时间 STWStop The WorldGC锁还是会在暂停期间过期其他线程可能拿到锁造成并发问题。所以分布式锁不是银弹对强一致要求很高的场景要慎重。至于 RedLock业内争议很大生产环境慎用我个人的态度是能用数据库唯一约束解决的不要先引入分布式锁。4.3 卷十五 过期删除与内存淘汰数据为什么“消失”了Redis 里的 key 一旦设置过期时间并不会立刻消失。它采用惰性删除 定期删除结合每次访问 key 时检查是否过期如果过期就删除同时后台定期抽样删除一批过期 key。这意味着某个过期 key 可能还会在内存里存在一段时间直到被访问或定期删除扫描到。当内存达到maxmemory上限Redis 会触发内存淘汰策略。常见策略有策略含义noeviction内存满后拒绝写请求直接报错allkeys-lru在所有 key 中按最近最少使用淘汰volatile-lru只在设置了过期时间的 key 中按最近最少使用淘汰allkeys-lfu在所有 key 中按访问频率最低淘汰volatile-lfu只在设置了过期时间的 key 中按访问频率最低淘汰volatile-random只在设置了过期时间的 key 中随机淘汰allkeys-random在所有 key 中随机淘汰实际生产里如果 Redis 只是缓存建议用allkeys-lru或者allkeys-lfu因为不依赖“是否设过期时间”这个条件淘汰空间更大。如果用volatile-*策略但业务里很多 key 没有设置过期时间内存满了可能无法淘汰任何 key结果只能写入失败。遇到“缓存里的数据突然没了”不要只怀疑 Redis 是不是被重启先检查maxmemory-policy和 key 是否被内存淘汰。从监控上可以看evicted_keys指标这个指标突增就说明内存不够用了。4.4 卷十六 大 Key、热 Key 与慢查询Redis 卡顿背后的真相最后这一卷说的是线上最容易出问题的三类情况。大 Key 指单个 key 的 value 特别大比如一个 Hash 有百万字段、一个 List 有几百万元素或者一个 String 的 value 有几十 MB。大 Key 的影响是双向的读的时候网络传输慢可能阻塞后续命令删的时候如果是DEL删除大 key 会阻塞主线程。现在 Redis 提供了UNLINK它把释放内存的操作放到后台线程执行删除时不会长时间阻塞。生产环境对确认无用的大 key优先用UNLINK。热 Key 指的是某个 key 的访问量极高比如秒杀商品、热点新闻。热 Key 会导致单节点 CPU 被打满而其他节点却很空闲因为 Redis 集群的数据分片是按 key 分布的热点 key 必然集中在某个槽位。治理手段包括在应用层加本地缓存把热 key 的读取拦在最前面或者给 key 加随机后缀拆成多个 key 分散到不同节点但代价是写入时也要写到多个副本过期时间要处理好。慢查询也是排查卡顿的入口。执行SLOWLOG GET 5可以看到最近 5 条慢命令。常见慢命令包括KEYS *、大数据量的HGETALL、SMEMBERS、ZRANGEBYSCORE返回大量元素、SORT等。不要在生产环境用KEYS *来查 key应该用SCAN分批迭代。这里的经验是出现偶发卡顿先看慢日志和latency monitor十有八九能找到元凶。最后分享一个排查 Redis 性能问题的实用技巧在命令行执行redis-cli --bigkeys它会扫描整个实例并输出每个类型中最大的 key 和最大元素数量。虽然这个命令在某些版本里可能触发较重的扫描建议在业务低峰期执行但用来发现大 Key 非常高效。我每次接手一个不熟悉的 Redis 实例第一件事就是跑一次这个命令再看缓存命中率和内存淘汰指标基本就能对实例的健康状态心里有数了。