很多准备面试的朋友一提到 Redis,第一反应都是“简单,背一背就完事了”。结果真到了现场,面试官一句“Redis为什么这么快”就能把人问住,再追问一句“那它单线程为什么还能这么快”,很多人就开始语无伦次了。其实Redis相关的面试题远不是背概念这么简单,它背后是一整套“存储系统设计”的取舍逻辑:内存、IO模型、持久化、高可用、一致性,每一块都能问得很深。这篇文章我把高频的Redis面试题整理成一条完整的复习线索,从数据结构、持久化到缓存三兄弟、高可用架构、分布式锁,再到日常运维里常见的大Key、热Key和阻塞问题,适合正在准备面试的同学照着过一遍,也适合刚接手Redis项目的朋友拿来当自查清单。
1. 先搞清楚面试官到底想考什么
1.1 一个开场问题带出全部主线:Redis为什么快
这是几乎所有Redis面试都会遇到的第一题。回答的时候不要把“快”当成一个孤立结论,要拆成几层来讲。
首先是纯内存操作。Redis把数据放在内存里,读写基本就是内存寻址级别,这比磁盘上的随机IO快了几个数量级,这是“快”的地基。其次是单线程模型配合IO多路复用,避免了多线程常见的锁竞争和线程上下文切换开销。很多人不理解“单线程为什么还快”,核心就在于Redis把“慢”的操作——网络IO等待——交给内核的多路复用机制去监听,主线程只在事件就绪时才去处理,CPU不会空转在等待上。再者是底层数据结构专门为高频访问做了优化,比如SDS字符串、跳表、哈希表,这些在读写路径上都尽量避免不必要的内存拷贝。最后还有渐进式rehash、对象共享这些小优化。
如果只回答“因为内存快”,这题最多拿一半分。面试官真正想听的是你能不能用一条主线把这些串起来:单线程 + 多路复用 + 内存访问 + 精简的数据结构,四个点彼此配合,才构成了Redsi高性能的完整解释。
1.2 把复习内容拆成四个层面,别只背答案
以我观察到的面试风格,Redis问题基本围绕四个层面展开。应用层:五种基础类型的用法、适用场景、过期策略怎么设。原理层:底层编码、持久化方式、单线程模型到底怎么跑。高可用层:主从复制、哨兵、集群分片是怎么协作的。权衡层:性能和可靠性怎么取舍,分布式锁是否绝对安全,缓存一致性要不要强保证。
不同年限的候选人侧重点完全不同。如果是一到三年经验,面试官主要考应用层和原理层;如果是高级岗位,一定会在权衡层反复追问,比如“你这么解决缓存穿透,代价是什么”“分布式锁真的安全吗”。准备面试时别只背“标准答案”,每个方案都想一下它的适用边界和缺点,这才是让面试官觉得你“用过、想过”的关键。
2. 数据结构与底层细节:别停留在背类型
2.1 五种基础类型的使用场景
Redis有五种最基础的类型,但面试不是考你把名字说全,而是考你能不能根据业务场景讲清楚为什么选它。我整理了一张表,基本够应付大多数场景题。
| 类型 | 本质 | 典型场景 |
|---|---|---|
| string | 字符串或整数 | 缓存、计数器、分布式锁、对象序列化 |
| hash | 字段-值映射 | 存储对象,如用户信息,支持单独更新某个字段 |
| list | 双向链表或压缩列表 | 消息队列、拉取时间线、最新列表 |
| set | 无序集合 | 去重、标签、交集并集运算 |
| zset | 有序集合 | 排行榜、延迟队列、滑动窗口 |
要特别注意hash和string存对象的区别。hash可以只更新某一个字段,不用把整个对象取出来再写回去;但如果对象本身很小、更新频率低,string配合序列化可能更简单高效。面试里让你“设计一个排行榜”,大部分人第一反应是zset,但如果面试官追问“分数相同怎么排序”,就要能答出zset的排序规则——默认按分数升序,分数相同再按字典序。这些细节才是加分点。
另外,现在面试也会聊到扩展类型,比如HyperLogLog做基数统计、GEO做附近的人、Stream做消息队列。不用背太深,但至少要知道在什么场景下会用它们,因为它们体现了Redis“不止缓存”的定位。
2.2 底层编码与SDS、跳表的细节
面试到了中间阶段,面试官往往会把话题引向底层:“string的底层是什么”“zset为什么用跳表,不用红黑树”。这些问题需要提前准备。
string的底层是SDS(简单动态字符串)。和C原生字符串比,它有几个很关键的优势:O(1)获取长度,不用遍历;不会出现缓冲区溢出,因为API会检查剩余空间并自动扩容;二进制安全,可以存包含空字符的数据;减少了修改字符串时的内存重分配次数,因为有预分配机制。最好用的面试表达是:“SDS就是为Redis的高性能字符串操作服务的,它把长度、容量都变成了结构体字段,而不是靠字符串结尾符去判断。”
zset底层是跳表。面试官几乎必问“为什么用跳表而不是红黑树”。跳表和红黑树的时间复杂度在查找上都接近O(logN),但跳表实现起来简单,节点按顺序链接,做范围查询特别自然——直接从头节点往后遍历就行,红黑树做范围查询需要中序遍历,代码复杂度高很多。Redis还需要支持按分值区间快速定位、按排名取元素,跳表和哈希表配合恰好能覆盖这些操作。面试时能说出“实现简单、范围查找友好”这两点基本就够。
还有两个底层细节也高频出现。一个是压缩列表ziplist和quicklist,list、hash、zset在元素少时会用压缩结构存储,省内存;元素变多或单个元素变大后才转为更复杂结构。另一个是哈希表的渐进式rehash,当哈希冲突多了,Redis不会一次性把所有桶重新映射,而是分批把老桶的数据往新桶迁移,避免大字典扩容时阻塞主线程。
3. 持久化机制:高频考RDB和AOF
3.1 RDB快照是怎么工作的
RDB是把内存数据以二进制快照形式写入磁盘。面试要答出两个核心命令:save和bgsave。save是主进程直接做快照,会阻塞所有请求,生产环境基本不用;bgsave是fork一个子进程出来做快照,父进程继续处理请求。
这里的考点是fork和COW(写时复制)。fork出来的子进程刚开始和父进程共享同一份内存页,不需要把整个内存拷贝出来,所以fork很快。真正开内存的地方在父进程处理写请求时:父进程一旦要修改某个内存页,就会把那一页复制一份,保证子进程看到的是fork时刻的稳定数据。代价是:如果fork之后写入非常频繁,父进程复制内存页的开销会很大,内存也可能短暂上升。这个问题值得在面试时主动说出来,面试官会觉得你理解得比八股文深。
RDB的优点是恢复快,文件紧凑,适合做冷备份和灾难恢复。缺点也很明显:它会丢数据,因为从上次快照到宕机之间的写操作不在文件里。所以RDB一般配合AOF一起用,或者从库上开RDB做备份。
3.2 AOF追加日志和重写机制
AOF记录的是Redis收到过的每一条写命令,以追加方式写入日志。面试重点有三个:写回策略、文件膨胀、重写。
写回策略通过配置appendfsync控制。always是每条命令都落盘,最安全但性能最差;everysec是每秒刷一次盘,最多丢一秒数据,性能和安全相对平衡;no让操作系统决定什么时候落盘,性能最好但可能丢大量数据。大部分场景用everysec就够了,这也是默认配置。
AOF最大的问题是文件会越来越大。重写机制解决的就是这个:通过读取当前内存状态,生成一组能重建这份数据集的最小命令集合,而不是在旧文件后面一点点追加。重写过程同样fork子进程来做,父进程把重写期间新产生的写命令缓存在一个专门buffer里,子进程完成后再追加进去。面试时能说出“重写并不是整理旧AOF文件,而是根据当前数据重新生成一个新文件”这句话,说明你真的理解重写。
3.3 混合持久化和常见追问
如果面试官继续追问“重启时到底加载哪个文件”,答案是优先AOF。因为AOF一般比RDB保存的数据更完整。新版Redis还支持混合持久化:把RDB快照作为AOF文件的开头,后面再跟一段AOF增量日志。这样重启后先加载RDB快速恢复,再用增量日志补上最近的数据,既快又能减少丢失量。
还有一个很常见的追问:“Redis持久化能保证完全不丢数据吗?”答案是不能,即使开了AOF everysec,极端情况也会丢一秒左右的数据。如果业务要求更严格,要么调整策略为always接受性能损失,要么从架构层面做双写或手动补偿。面试里承认“做不到完美,但可以用多种手段逼近”,比拍胸脯说“绝对不丢”要可信得多。
4. 缓存穿透、击穿、雪崩:必考组合题
4.1 三个问题怎么区分
我见过太多人把穿透和击穿说反。穿透是请求一个缓存里不存在、数据库里也不存在的key,导致每次请求都穿过缓存层直接打到数据库。击穿是缓存里某个热点key正好过期,同一时刻大量请求涌进来,全部打到数据库。雪崩是大量key一起过期,或者缓存节点整体不可用,导致数据库瞬间被压垮。
| 问题 | 触发条件 | 核心表现 |
|---|---|---|
| 缓存穿透 | key在缓存和数据库都不存在 | 每次请求都穿透到DB |
| 缓存击穿 | 单个热点key过期瞬间 | 大量并发打同一个DB key |
| 缓存雪崩 | 大量key同时过期,或节点宕机 | 大面积请求打到DB |
回答的时候建议先画一个场景,比如“用户传了一个不存在的商品ID”,再展开方案。这样面试官不用猜你脑中的模型,你说的每句话都能对上号。
4.2 各自的应对手段
缓存穿透的经典解法有三个:第一,把null值也缓存起来,设置较短的过期时间,防止大量无效请求穿透;第二,使用布隆过滤器,在缓存前先判断key是否可能存在,不存在就直接返回;第三,接口层做基本参数校验,明显非法的请求直接拦截。布隆过滤器有误判率,它只能保证“不在集合里的肯定不在”,但可能把不存在的误判成存在,所以并不能完全取代空值缓存。
缓存击穿的核心解法是互斥锁。缓存过期后,只允许一个线程去数据库回源,其它线程等待这个线程把缓存重建好后再读取。可以用Redis的分布式锁实现,也可以用本地锁,视应用节点数量而定。另外比较实用的是热点key“逻辑过期”方案:缓存不设置物理过期时间,而是把过期时间存在value里,访问时发现逻辑上要过期了再异步去刷新,请求返回旧值兜底,这样不会在刷新期间出现空窗期。
缓存雪崩的应对可以从三个角度展开。缓存角度:过期时间加随机值,避免同时过期。架构角度:加多级缓存,本地缓存扛住第一波,降低对Redis的依赖。数据库角度:做限流、熔断、降级,高峰期让数据库压力可控。很多人只答“过期时间加随机”,应对单点故障导致的雪崩就没覆盖到,所以一定要提到高可用部署和降级方案。
5. 过期删除与内存淘汰:容易混淆的两个机制
5.1 过期键是怎么被删的
Redis删除过期key采用的不是单一策略,而是“定期删除 + 惰性删除”组合。
惰性删除很好理解:每次访问key的时候,检查它是否过期,过期就删掉再返回空。好处是不用额外占用CPU,坏处是如果很多key一直不被访问,它们会一直占着内存。定期删除就是为了弥补这一点:Redis每隔一段时间随机抽一批设置了过期时间的key,把其中过期的删掉。它不是全量扫描,而是随机抽查,所以即使定期删了,仍然可能有少量过期key残留,等下次被访问时再惰性删除。
这里有个加分知识点:主从架构下,过期key的删除是主库主导的。主库发现key过期后删除它,并把这个删除操作传播给从库,从库不会自己主动删过期key。所以从库读可能会读到已经过期但还没收到删除命令的数据,在强一致要求下需要考虑这个问题。
5.2 内存淘汰策略怎么选
当Redis内存用到maxmemory上限时,会走淘汰策略。面试的回答重点是几个常用策略:noeviction不淘汰任何人,写请求会直接报错;allkeys-lru从全部key里按最近最少使用淘汰;volatile-lru只从设置过期时间的key里淘汰;allkeys-random随机淘汰;volatile-ttl优先淘汰剩余存活时间最短的key;还有LFU相关的策略,比如allkeys-lfu按访问频率淘汰。
面试里容易被难住的问题有两个。第一个:“LRU和LFU有什么区别?”LRU看最近一次访问时间,LFU看一段时间的访问频率。如果某个key在过去是一个热点但现在已经不热了,LRU可能因为它最近刚被访问而保留,LFU则会逐渐降权。对业务长期稳定的热点,LFU更准。Redis的LRU其实是近似LRU,它不会维护全量访问时间链表,而是抽样后比较,减少内存占用。
第二个:“如果设置的是noeviction,从库会怎么样?”这个提醒一下:从库的淘汰行为受主库控制,主库淘汰key后会向从库发送删除命令,从库一般不会自行淘汰。不过实际项目中没人会长期用noeviction,除非把Redis当纯缓存以外的重要存储用,这时宁可不写也不能丢数据。
6. 主从、哨兵、Cluster:高可用三件套
6.1 主从复制与同步细节
主从复制的面试题,一般从“slave怎么同步master的数据”开始。全量同步流程是:从库发送psync给主库,带上上一次的复制ID和偏移量;主库发现对不上,就返回fullresync并触发bgsave生成RDB快照,把快照发给从库;从库清空旧数据,加载快照;主库同时把快照期间新收到的写命令放进复制缓冲区,从库加载完RDB后,再补发这些增量命令,完成对齐。
部分重同步是更精细的考点:如果从库只是短时间断线,重连后不需要再全量同步,只要主库的复制积压缓冲区里还存有从库缺失的命令,就把这部分增量发过去,避免反复传大快照。如果断线时间太长,积压缓冲区已经被覆盖,那就只能再全量同步。
复制是异步的,这一点很容易被忽略。主库写完一条命令后立即返回成功,不会等从库确认。这样性能高,但主从切换时可能丢数据。如果面试官问“主从延迟怎么办”,可以从读场景接受轻微延迟、关键操作强制读主库、或者监控复制积压和偏移量这几个方面来答。
6.2 哨兵是怎么做故障转移的
哨兵解决的是主库挂了怎么自动切换的问题。它有三个职责:监控、通知、自动故障转移。
面试时重点说清楚两个概念:主观下线和客观下线。单个哨兵发现ping主库超时,先标记主观下线。为了避免网络抖动误判,需要多个哨兵之间达成一致,当超过quorum数量的哨兵都认为主库下线时,才标记客观下线,触发故障转移。这个设计本质上是把“单点判断”变成“分布式投票”,降低误判概率。
故障转移第一步是选出新主库。选新主的规则一般按优先级、复制偏移量、runid大小排序。随后哨兵会通知所有从库去复制新主,再通知客户端更新主库地址。这里有一个值得准备的坑:旧主库在断网期间可能还在接收写请求,网络恢复后它已经变成新集群里的从库,这部分多余的数据会被丢弃,也就是脑裂丢数据。生产中可以通过配置min-replicas-to-write,让旧主库在从库失联时拒绝写入,降低脑裂风险。
6.3 Cluster集群和哈希槽
Cluster解决的是数据量大到单机撑不住时的水平扩展问题。它的核心是16384个哈希槽,每个key通过CRC16算法算出一个数字,再对16384取模,决定落在哪个槽;槽再被分配给不同的节点。所以Cluster存数据时不是一个key对应固定节点,而是一个key对应一个槽,槽才是迁移的最小单位。
有两个细节面试者常忽略。一是客户端访问Cluster时,如果请求的key不在当前节点,会收到MOVED错误,客户端要根据响应里的地址去正确的节点访问;如果是槽正在迁移,可能收到ASK错误,数据迁完前需要临时去目标节点找。二是用hash tag可以把多个key放到同一个槽里,从而在集群模式下支持多key操作,比如{mall:cart}.{user123}这样的key,只有花括号里的内容参与哈希计算。
面试官如果问“Cluster和哨兵模式怎么选”,答案是看容量和扩展需求。哨兵只是高可用,数据都在单主节点上;Cluster是分片存储,可以把写入压力分摊到多个节点。数据量和写入吞吐已经超过单机能力,就上Cluster;只是想要自动切换,哨兵就够了。
7. 分布式锁:答得深的人很少
7.1 单实例分布式锁的基本实现
先答最基础、最常用的一种。用SET key value NX PX 30000加锁,NX表示只有当key不存在时才设置成功,PX设置过期时间。value要放一个全局唯一标识,比如UUID或者业务请求ID,目的是释放锁时确认“这把锁是自己加的”。
释放锁不能简单用DEL,因为存在一个经典问题:线程A持有锁,代码执行时间超过过期时间导致锁自动释放,线程B拿到锁,这时A执行完了调用DEL,把B的锁误删。解决方式是先用Lua脚本判断value是否等于自己的唯一标识,相等才删除,保证“判断+删除”是一个原子操作。
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end很多人提到SETNX加锁后就结束了,其实面试官想听到你考虑过期时间续期、误删旧锁、阻塞太长怎么办。一个经验做法是:不要在业务里写死一个很小的过期时间,而是用一个后台线程定期给锁续期,业务结束后主动释放。Redis官方也有现成客户端组件的看门狗机制,原理类似。
7.2 分布式锁的边界和Redlock争议
如果面试官继续深挖,会问“单实例分布式锁挂在机器宕机了怎么办”。这时可以引到Redlock:对多个独立Redis节点依次加锁,只有大部分节点都加锁成功并且总耗时小于锁有效期,才算成功获取锁。它通过多数派来抵抗单节点故障。
但要诚实指出Redlock并不是“绝对可靠”。时钟跳跃、进程GC暂停、网络分区都会让多个客户端同时认为自己持锁,这个在现实中没办法完全消除。社区对这个方案一直有争议,实际工程里更稳妥的做法是:能接受极小概率锁冲突的业务,单实例锁足够;需要更强保证时,考虑使用具备共识算法的分布式协调组件,而不是自己造Redlock轮子。
面试里最忌讳的是把分布式锁说得像“绝对互斥”。只要你能说清楚它“在大多数场景下可用,但在极端故障下可能失效,所以业务侧要做好防护”,面试官会明显认可你的判断力。
8. 事务与Lua脚本:原子性不等于回滚
8.1 MULTI和EXEC的坑点
Redis事务用MULTI、EXEC、DISCARD、WATCH控制。MULTI开始后,命令不会立刻执行,而是进入队列,EXEC时一次性按顺序执行。如果队列里某条命令出现语法错误,EXEC会直接报错,整组命令不会执行;但如果是运行时错误,比如对一个字符串执行了list操作,其他命令依然会照常执行,不会回滚。
所以面试时不要用“事务就是要么全部成功要么全部失败”这种话来描述Redis事务。它更像“一批命令的批量执行”,使用WATCH可以实现乐观锁:在MULTI之前WATCH一个key,如果该key在EXEC之前被其他客户端修改,事务会被取消。适合做类似“先检查再更新”的场景,比如防止库存超卖。
8.2 Lua脚本的原子执行
Lua脚本是Redis开发里避不开的话题。用EVAL执行一段Lua脚本,Redis会保证脚本执行期间不会被其他命令插入,中间不会发生其他客户端请求的处理。这比事务的“批量执行”语义更实用,因为它可以用来完成多个步骤的原子操作,比如先判断再扣减、先取后删。
很多高并发场景的库存扣减、限流计数都是这么做的。脚本还可以减少网络往返,把多个命令放进一次调用。要注意的是,脚本里的key必须通过KEYS数组传进去,不能直接在脚本字符串里写死key,否则集群模式下Redis无法确定这些key分布在哪个槽,会直接报错。这也是生产环境踩坑的高频区。
面试如果聊到事务和Lua,最后可以主动提一句“大部分需要事务语义的场景,我更倾向用Lua脚本来实现”,因为脚本的原子性更清晰,缺点是开发者需要维护脚本代码。
9. 日常排查与实战经验:临场加分项
9.1 大Key问题的排查和治理
大Key通常指单个value特别大,比如一个string超过几百KB,或者一个hash、set里有几十万个元素。危害不只在内存占用,更严重的是访问和删除都会造成阻塞。比如用DEL删一个几百万成员的set,主线程会长时间卡住,大盘直接报警;用GET一次拉一个大key,网络和内存都压力巨大。
排查手段最常用的是redis-cli --bigkeys,它会扫描所有key并按类型输出占用靠前的key。生产环境跑这个命令建议放在低峰期,因为会有遍历开销。发现大Key之后,处理思路有几种:拆成多个小key,把集合按维度拆分;压缩value,比如用序列化压缩或者只存必要字段;对不常访问的历史数据,考虑淘汰或迁移到冷存储。
删除大Key还有一个反直觉的技巧:不要用DEL,要使用UNLINK或者分批删除。UNLINK是异步删除,主线程把释放内存的工作交给后台线程,不会卡住主线程。如果客户端版本太老不支持UNLINK,批量场景可以用SCAN配合逐批删除。
9.2 热Key带来的倾斜问题
热Key是指某个key被高频访问,超过单节点的承载能力,导致集群里个节点CPU高、网络打满,其他节点却很空闲。这就是数据倾斜,和“慢查询”不是一类问题。
排查热Key可以借助Redis自带的hotkeys参数,或者用MONITOR命令统计高频命令,但MONITOR本身有性能开销,线上慎用。处理方式常见的有几种:本地缓存扛一部分流量,把热门数据在应用内存里放一层;把同一个key复制成多个带后缀的副本,比如加1、2、3后缀分散到不同节点;读写分离让读请求分散到从库。热Key问题没有一个通用解,核心思路就是“让同一份热点数据能出现在多个地方,而不是全部压在同一个节点”。
9.3 慢查询和阻塞问题的排查思路
面试最后聊到“你遇过Redis什么故障”,很多人会说慢查询、阻塞,但答得比较泛。可以这样说:先用SLOWLOG GET查看慢日志,结合slowlog-log-slower-than阈值定位慢命令;然后看那些明显会阻塞的命令有没有混到线上,比如KEYS *、HGETALL大hash、SORT、FLUSHALL。生产里不应该出现KEYS *,应该用SCAN游标遍历。还要关注内存碎片率MEMORY FRAGMENTATION RATIO,过高说明大量过期key清理后内存没完全还给系统,可以计划重启或用jemalloc配置优化。
如果是业务侧的排查,我习惯先把问题归类:CPU较高就先看热Key和大Value,延迟抖动就看慢查询和fork次数,连接数暴涨就看客户端连接配置和阻塞命令。把方向定位到具体那一类,再开工具细查,比盲目使用命令要高效得多,这类实战经验在面试中很加分。
最后再分享一个我自己准备这类面试的体会:单纯背题很容易死在“再深入一点”的追问里。Redis的很多设计其实都在回答两个核心问题——怎么不让主线程被阻塞,怎么尽量不丢数据。拿这两个问题当主线去套知识点,你会发现回答任何一道题都能自然延伸,而不是一个词一个词往外蹦。