Redis面试题全解析:从数据类型到分布式锁,原理与实战
2026/9/15 18:57:51 网站建设 项目流程

“Redis面试题”这四个字,光念出来就能让不少后端候选人心里颤三颤。我这些年作为技术面试官,每年面过的Java后端候选人少说也有一百多个,Redis基本上是必问项。有人能把《Redis设计与实现》背得滚瓜烂熟,可一旦追问到“跳表为什么比红黑树合适”“主从切换丢锁怎么办”就明显卡壳;也有人平时只写CRUD,但能结合自己的业务场景把布隆过滤器、延迟双删讲得明明白白。这背后差的不是记忆力,而是理解深度。

这篇文章我把实际面试中命中率最高的一批Redis问题梳理了出来,既给答案,也把答案背后的原理讲透。适合准备跳槽面试的同学,也适合平时写业务但想系统补一补Redis基础的人。我不打算按教科书顺序罗列,而是按面试官出题的习惯来组织,尽量还原真实问答现场。

1. Redis的“地基”:先从数据类型问起

1.1 五种基础类型怎么答才显得有深度

面试开场最常见的问题就是,“Redis有哪些数据类型?”这个问题看起来人畜无害,但拉开差距的地方恰恰在这里。只回答String、Hash、List、Set、ZSet,这是及格线以下。真正能拿分的回答,是连用途场景一起带出来。

Redis五种基础数据类型,简单说就是:

  • String:最常用的类型,可以存字符串、整数、浮点数,典型场景是计数器、分布式锁、Session共享、热点数据缓存。底层其实不是简单的C字符串,而是一种叫SDS的结构。
  • Hash:适合存对象,比如用户信息、商品详情。可以像操作数据库表一样单独更新某个字段,不用整个对象序列化后整体覆盖。
  • List:双向链表,支持两端push/pop。经典用法是实现简单的消息队列、最新列表(比如朋友圈时间线)、关注列表分页。
  • Set:无序且自动去重,适合做标签系统、抽奖池、社交场景里的“共同好友”计算。
  • ZSet:在Set基础上给每个元素加了一个score分数,按分数排序,最适合做排行榜、延迟队列、限流窗口统计。

但光会这些还不够。真正的加分项是底层编码。比如String在底层会根据内容长度和类型,选择int、embstr、raw三种编码方式;Hash如果元素少且值小,会用listpack(Redis 7.0之前是ziplist)来节省内存,当元素数量超过阈值时才转为hashtable;ZSet在数据量小的时候也用listpack,大了才用跳表加字典的组合。我面试时会特意追问这些编码细节,因为能答上来的人,说明真的看过源码或者深入过底层,而不是只背了面试题。

1.2 底层数据结构:SDS、跳表、压缩列表

这里先说SDS,也就是简单动态字符串。Redis没有直接用C语言的char数组来存字符串,而是自己封装了一层。SDS的基本结构包含len长度字段、alloc分配空间字段和一个char数组。这么做的好处有三个:

第一,获取字符串长度的时间复杂度从O(n)变成O(1)。C字符串要遍历到结尾的\0才知道长度,SDS直接读len字段就行。

第二,避免了缓冲区溢出。C字符串拼接时不检查空间,容易覆盖相邻内存。SDS在拼接前会先通过alloc检查剩余空间,不够就自动扩容。这和我们平时写Java时用ArrayList不用原始数组是一个道理,都是让你不操心容量问题。

第三,二进制安全。C字符串以\0判断结尾,所以字符串里一旦出现\0就会被截断。但Redis里存的可能是序列化后的Java对象、图片等二进制数据,里面完全可能有\0。SDS因为用len字段记录长度,不依赖\0,所以什么数据都能存。

再说跳表。ZSet在数据量大的时候会同时使用字典和跳表,字典负责按成员快速查分数,跳表负责按分数范围做有序遍历。面试官在这里常问一句:“为什么选跳表而不选红黑树或者B+树?”我的回答思路是:跳表实现简单,增删改查都是O(log n),但代码复杂度比红黑树低很多;B+树更擅长磁盘IO优化,但Redis是纯内存操作,不需要考虑磁盘页访问,跳表足够了。再加上跳表做范围查询很灵活,只需要在找到起点后沿指针往后走就行,这就是为什么排行榜这种需求它能轻松搞定。

1.3 为什么Redis命令执行是单线程,还能这么快

“Redis是单线程的吗?”这个问题现在不能一口答死。正确的说法是:Redis的命令执行核心是单线程的,但Redis 6.0开始引入了多线程IO,网络读写可以用多线程来处理,不过实际执行命令还是单线程。早几年面试官问“为什么Redis单线程还快”,标准答案基本是:纯内存操作、避免多线程上下文切换和锁竞争、使用IO多路复用驱动事件循环。

这背后的真实场景是,Redis的瓶颈几乎不可能出现在CPU上,而是网络IO和内存。如果用多线程,反而要面对共享数据加锁、线程切换开销,对于Redis这种命令处理极其简单的场景是得不偿失的。拿日常经验类比:一个人记账不需要团队协作,加锁协调反而更慢。IO多路复用通俗解释就是,一个线程同时盯着成千上万个网络连接,当某个连接有数据可读时才处理它,没有数据就继续等,这比每个连接开一个线程高效太多了。

面试官如果追问“单线程为什么能保证原子性”,你可以顺势答:因为命令执行过程不会被其他命令打断,所以像INCR这种操作天然没有并发问题,也就不需要加锁。这为后面分布式锁的提问埋下了伏笔。

2. 缓存三大难题:穿透、击穿、雪崩

2.1 缓存穿透:一直在查数据库里不存在的东西

缓存穿透的意思是,请求的数据在缓存和数据库中都不存在,导致每次请求都直接打到数据库。打个比方,有人不断用不存在的用户ID去查用户信息,缓存里没有,数据库里也没有,结果每一次请求都穿透了缓存这层防护,数据库压力瞬间拉满。

最常见的场景就是恶意攻击或者非法参数请求。我在面试中会问候选人:“如果不做任何防护,一个循环脚本用不存在的ID刷你接口,你的数据库扛得住吗?”很多人这时才意识到问题的严重性。

解决方法主要有三种。第一种是缓存空值,也就是当数据库查不到数据时,也把这个“空结果”缓存起来,设置一个较短的过期时间,比如30到60秒。这样做的好处是简单直接,缺点是会有大量空值占据缓存空间,需要配合设置合理的过期时间。第二种是使用布隆过滤器,在请求到达数据库之前,先用布隆过滤器判断这个key是否存在,如果过滤器说“一定不存在”,就直接返回。布隆过滤器的原理是多个哈希函数映射到位数组上,可能误判但绝对不会漏判,所以只能过滤掉那些确定不存在的请求。第三种是最简单也最容易被忽视的,接口层的参数校验,非法参数直接返回错误,不给后边任何机会。

2.2 缓存击穿:热点key过期的瞬间

缓存击穿和穿透很多人容易搞混。击穿针对的是某一个热点key,它在缓存中过期的那一瞬间,大量并发请求同时涌入,结果发现缓存没有数据,就一起打到数据库。这种问题特殊在“单个热点key”上,而不是“大量不存在的key”。

举个例子,一个秒杀商品详情页,平时缓存命中率很高,所有流量都靠Redis扛着。突然这个商品的缓存到期了,这时候一千个请求同时进来,发现缓存Miss,全部冲向数据库,数据库就挂了。

解题思路也很有代表性。第一种是互斥锁,也就是缓存失效后,不是所有请求都去查数据库,而是先尝试获取锁,只有一个线程能拿到锁并去数据库查询,其他线程阻塞等待一段时间后重试。这种方式实现简单,但会引入额外延迟,而且要注意防止死锁和锁误删。第二种是逻辑过期,不设置真正的过期时间,而是把过期时间作为value的一部分存在缓存里。查询时如果发现逻辑上过期了,可以返回旧数据的同时,让一个后台线程去刷新缓存。这种方案的好处是用户几乎无感知,但实现复杂度高一点。第三种方式比较取巧,就是把热点key的过期时间在业务上主动延长,对于可能成为热点的key,在过期前预先做一次缓存预热,避免让它裸奔。

2.3 缓存雪崩:大面积失效或Redis不可用

缓存雪崩指的是大量key在同一时间段集中过期,或者Redis本身宕机,导致所有请求都落到数据库上。它不是单点问题,而是面状问题。典型场景就是某个活动零点开始,我们提前把大量活动数据写入缓存,过期时间统一设为凌晨一点,结果一点一到,海量key同时失效,数据库被一波流带走。

解决方案有几个方向。第一,过期时间加随机值。比如在固定过期时间上加上一个随机秒数,让key的过期时间错开,避免齐刷刷失效。这是成本最低也最常用的手段。第二,使用多级缓存。Redis之上再加一层本地缓存(比如Caffeine),就算Redis出问题,本地缓存还能挡掉一部分流量。第三,依赖Redis高可用架构,比如主从加哨兵、Cluster集群,避免Redis实例挂掉后完全没有缓存可用。

面试中如果把这三个问题放在一起对比,应该这样区分:穿透是查询一个不存在的数据;击穿是一个热点key过期瞬间的并发冲击;雪崩是大量key同时过期或者Redis不可用。这三句话是答题框架,框架后面加上对应方案里的细节,这场对话就有血有肉了。

3. 数据不能丢:持久化、主从复制、哨兵与集群

3.1 RDB和AOF怎么选

“Redis挂了会丢数据吗?”这个问题是所有后端面试逃不掉的。Redis虽然叫缓存,但如果只把它当缓存,丢数据无所谓,可当你用它存Session、存订单号、存分布式锁时,丢数据就是事故了。因此持久化是必问项。

RDB持久化机制生成的是二进制快照文件,它是某个时间点上的全量数据。触发方式可以手动执行SAVE或BGSAVE,也可以在配置里设置类似“900秒内至少1次写操作就触发”这样的规则。BGSAVE会fork一个子进程来生成快照,主进程不阻塞,这是它的核心优势。但RDB的劣势也明显,两次快照之间的数据,在宕机时必然会丢。

AOF则是追加写命令日志,每次写操作都记录到文件里。AOF的刷盘策略有三种:always表示每条写命令都立即同步到硬盘,最安全但性能最差;everysec表示每秒同步一次,性能和数据安全比较均衡,最多丢一秒数据;no表示交给操作系统决定何时刷盘,性能最好但丢数据最不可控。默认建议使用everysec,这也是Redis的默认配置。

到这里面试官通常会追问:“那到底用RDB还是AOF?”现在的实践中,比较好的方案是用RDB做冷备与恢复,开启AOF保证数据可靠性,而且Redis 4.0之后支持混合持久化。混合持久化的意思是,在AOF重写时把当前数据以RDB格式写入AOF文件,后续增量命令再以AOF日志追加进去。这样既能利用RDB恢复速度快的优点,又不会丢失太多增量数据,文件大小也得到了控制。面试中能提到混合持久化,说明你对Redis版本演进是有概念的,这一点比较容易拿好感分。

3.2 主从复制的完整流程

主从复制面试题的核心考点是:全量同步和增量同步是怎么配合的。主从复制刚建立,或者从节点断开太久导致复制缓冲丢失时,会触发全量同步。主节点生成RDB快照发给从节点,同时把所有收到的写命令缓存在复制缓冲里,等从节点加载完快照后继续发送这些增量命令。新版本的Redis使用PSYNC命令,可以做到断线重连时,如果数据还在复制积压缓冲区里,就只做增量同步,省掉全量同步的巨大开销。

这里有个细节值得记住:复制积压缓冲区是一个环形的内存队列,大小由repl-backlog-size参数控制,默认是1MB。如果从节点断线时间太长,积压缓冲区里的数据被新写入覆盖了,那只能退回全量同步。所以生产环境要结合业务数据量合理调大这个值,不然从节点偶尔抖动一下,主节点就得磁盘IO拉满做一次全量RDB,代价相当大。我在实际排障时曾经遇到过一个线上事故,主从节点频繁断连,就是因为备份缓冲区太小,从节点每次重连都触发全量复制,主节点CPU和磁盘都被打满,形成恶性循环。

3.3 哨兵与Cluster集群

哨兵模式下,Redis的数据其实存在主从节点上,哨兵节点负责监控。当主节点发生故障时,哨兵集群通过投票选出一个哨兵leader,再从从节点中选举一个提升为新的主节点,这个过程叫故障转移。面试时最容易考的是主观下线和客观下线的区别:主观下线是一个哨兵自己觉得某个节点不可达,客观下线是多个哨兵都认为不可达,达到quorum阈值后才会真正触发故障转移。

Cluster集群则把数据分为16384个槽位,每个节点负责一段槽位。写入数据时,Redis会对key做CRC16校验并取模,计算出这个key应该落在哪个槽位,然后由该槽位所在的节点处理。如果客户端连的节点不是负责这个槽位的节点,它会返回MOVED错误,客户端收到后要重定向到正确节点。这里还有一个点,Cluster模式下只用db0,不能多选数据库,因为槽位计算不支持多db。

被问到“为什么Cluster不用一致性哈希而是用槽位”时,回答思路是:固定数量槽位让数据分布更均匀,也更容易实现节点间的数据迁移。Redis Cluster迁移数据的最小单位是槽位,加上每个槽位里的数据,扩容缩容只涉及槽位在节点间重新分配,复杂度比一致性哈希里的虚拟节点方案好控制得多。

4. 分布式锁:从手写SET NX到Redisson

4.1 手动实现Redis分布式锁

分布式锁是Redis面试题里的高频压轴题,几乎每三场面试就会遇到一次。它的出场背景是:在单机应用里,多线程竞争资源可以用synchronized或者ReentrantLock;但在微服务架构下,多个服务实例跑在不同机器上,JVM锁就管不到其他进程了,这时候需要一个跨进程的互斥机制,Redis分布式锁就是最常见的选择。

最原始版本的实现是SETNX加EXPIRE两步操作。SETNX表示只有key不存在时才能设置成功,谁设置成功谁就拿到了锁。但这样做有一个明显问题:两步操作不是原子的,如果SETNX成功之后,进程在设置EXPIRE之前挂掉了,锁就永远不会释放,其他线程全部死等。

所以后来才有了SET加NX加EX的原子命令:SET lockKey lockValue NX EX 30。这条命令把加锁和设置过期时间合在一起执行,Redis单线程执行命令,天然不会出现中间状态。加锁时说清楚了,释放锁也同样有坑。如果直接DELETE锁,可能会把别人持有的锁误删。比如线程A的锁已经超时自动过期了,线程B拿到了新锁,这时候线程A处理完业务执行DELETE,就会把B的锁删掉。解决方案是在value里存放一个唯一标识,比如UUID,释放锁之前先GET比较是不是自己的值,用Lua脚本保证“比较值+删除”这两个操作的原子性。

手写这种锁虽然能跑,但有几个边界问题处理起来很麻烦:锁过期时间设多少合适?业务执行超过锁过期时间怎么办?重入怎么办?所以面试中手写版本讲完之后,一定要把话题引向生产级方案。

4.2 Redisson的看门狗机制怎么工作

Redisson是Java生态里最常用的Redis客户端,它提供的RLock解决了手写锁的很多痛点。我第一次了解Redisson看门狗机制的时候,觉得这个设计确实聪明。它的逻辑是:如果业务方没有给锁指定leaseTime,那么锁默认的过期时间是30秒,但Redisson会启动一个后台定时任务,每过10秒(也就是锁过期时间的1/3)就自动给这把锁续期到30秒,保持持有状态。这样就算业务方法执行时间超过30秒,锁也不会被提前释放,直到业务执行完,主动释放锁时才把后台续期任务取消掉。

这就是“看门狗”这个名字的来历,锁快过期了,就帮你续一把。如果没有这个机制,你就只能自己估算业务执行时间,然后手动设置一个足够大的过期时间,可设大了万一持有锁的节点宕机,锁要等很久才能被其他人拿到;设小了业务还没执行完锁就没了,并发安全问题随之而来。看门狗相当于帮你在两个极端之间找到了平衡。

Redisson还支持可重入。同一个线程可以多次获取同一把锁,内部用计数器记录重入次数,每次释放锁时计数器减一,减到零才真正删除锁。这点在很多递归或嵌套调用的场景里很有用,比如一个方法内部循环调用另一个需要加锁的方法,可重入性就能避免自己把自己锁死。

4.3 RedLock的争议与主从切换丢锁问题

讲完Redisson,面试官可能会祭出一个更刁钻的问题:“假如主节点在加锁成功后立刻宕机,但数据还没同步到从节点,这时从节点升级成主节点,锁不就丢了吗?”这是分布式锁在主从复制架构下一个很尖锐的问题。Redisson在普通主从模式下确实不能完全解决这个问题,所以才会有人提出RedLock算法。

RedLock的思路是,部署多个独立的Redis节点,比如5个,加锁时必须向所有节点都执行SET NX EX,只要成功加锁的节点数超过一半,就认为加锁成功。这个设计参考了Quorum机制,用来应对少数节点故障时锁依然可靠。但RedLock在业内是有争议的,著名分布式系统专家Martin Kleppmann专门写文章批评过它,认为它在某些场景下依然存在安全性问题,比如GC暂停导致持有锁时间超过过期时间,或者时钟漂移导致节点间判断不一致。Redis作者antirez也写了文章回应,说Redis分布式锁的定位是高可用场景而不是绝对强一致。

面试里如果能把这场争论说出来,并且表明自己的观点,是很加分的。我的建议是,一般的业务场景,Redisson普通分布式锁已经足够;如果对锁的安全性要求极高,不妨考虑其他强一致协调服务,不必死磕Redis。这个分寸感体现的是工程能力,不是背书能力。

5. 内存管理、大Key与序列化

5.1 过期策略和内存淘汰的区别

很多候选人会把“过期策略”和“内存淘汰策略”当成一回事,其实是两个层面的问题。过期策略是处理那些设置了TTL的key,到期了怎么把它删除的问题;内存淘汰是当Redis内存达到maxmemory上限时,按什么规则腾出空间的问题。

Redis的过期删除策略是惰性删除加定期删除的组合。惰性删除的意思是,每次访问某个key时,先检查它有没有过期,过期了就删除。这样可以省下大量CPU资源,因为不会主动去扫所有key。但问题是,如果有些key设置了过期时间却一直没人访问,就会一直堆在内存里。所以Redis搭配了定期删除:每隔一段时间,从设置了过期时间的key里随机抽取一批,删除其中过期的,然后继续下一轮。这是一个概率型策略,目的是通过少量CPU开销回收大部分过期key。

内存淘汰则是另一套规则。Redis 8种淘汰策略里,默认是noeviction,也就是内存满了直接拒绝写入。生产环境最常用的有allkeys-lru和volatile-lru。前者在全量key里按LRU算法淘汰最久未使用的,后者只在设置了过期时间的key里做淘汰。Redis 4.0之后新增了LFU策略,也就是根据key的访问频率来淘汰,而不是单纯看访问时间。用LFU的场景通常是区分缓存热点和少量高频key,比LRU更能抵抗“每次批量扫key导致缓存污染”的情况。

面试中建议准备一个记忆框架:先看策略是作用于所有key还是只作用于设置了过期时间的key,再看淘汰依据是最近最少使用还是最少频率使用,还是随机,这样就不会混了。

5.2 线上Redis变卡,先查大Key和热Key

“线上Redis偶尔延迟暴涨,怎么排查?”这是我在实际面试中特别爱问的一道开放题,因为它很考验排障经验。答案通常绕不开大Key和热Key两个坑。

大Key是指单个key对应的value过大,比如一个String类型存了几MB的数据,或者一个Hash里有几百万个字段。大Key的危害在于:Redis执行命令是单线程的,操作一个几MB的key,会让这个命令的执行时间变长,期间所有其他命令都要排队等待。另外,删除一个大Key也可能阻塞主线程,比如DEL一个超大集合时,删除操作本身就很耗时。解决办法是,生成大Key的源头改造成多个小Key,或者在删除时用UNLINK异步删除。排查命令是redis-cli --bigkeys,它会扫描整个实例并统计出最大的key。

热Key是指某个key在极短时间内被大量访问,比如某个明星的置顶微博、某个秒杀商品。它的问题是会把单台Redis节点的CPU和网络带宽打满,出现局部过热。解决方案包括:在应用层对这个热Key做本地缓存,把压力挡在Redis之前;对Redis热Key做多副本缓存,把一个key拆成多个带后缀的key分散到不同节点;或者做好读写分离,用从节点分担读压力。

这个知识点放到简历里,面试官一般会觉得你是有真实排障经验的,因为单纯背面试题说不出来大Key删除会阻塞主线程这种话。

5.3 对象序列化:为什么总有人在Redis里存不下Java对象

Redis本身只能存字符串和二进制数据,Java对象想放进Redis必须序列化。这块面试题的热搜度一直很高,因为实际项目里总会碰到。

常见的序列化方案有JdkSerializationRedisSerializer、JacksonJsonRedisSerializer、FastJsonRedisSerializer和Protobuf等。JDK自带序列化最省事,但问题也很明显:序列化后的结构包含大量的类描述信息和包路径,非常占空间;而且只能被Java反序列化,跨语言基本没戏。我见过一个项目,把一个大对象用JDK序列化后存Redis,同一个对象用JSON存只有几十KB,JDK序列化后能到几百KB,内存存储成本直接翻好几倍。

更坑的是,JDK序列化还有类版本兼容问题。如果你修改了实体类的字段,旧的缓存数据反序列化时会直接报错,老缓存全部失效,就得清掉重来。所以实际项目中我基本只用JSON类序列化,Jackson或Fastjson都行,存出来可读性好,跨语言也没问题。真正有性能要求的场景,可以考虑Protobuf这类二进制序列化,但需要维护proto文件,成本高一些。

面试时如果被问到“Redis序列化应该注意什么”,回答方向大概是:考虑可读性和体积、考虑跨语言需求、考虑类结构变化后的兼容性。如果能再补一句“JDK序列化会把类元数据一起存进去,导致空间浪费”,基本就稳了。

6. 缓存一致性:业务里最头疼的延伸题

6.1 Cache Aside模式为什么先更新数据库再删缓存

缓存与数据库的一致性问题,一般会在面试末尾被抛出来,用来考察候选人解决实际问题的思路。最经典的模式是Cache Aside(旁路缓存),读的时候先读缓存,没有就读数据库然后回填缓存;写的时候更新数据库,然后删除缓存。

这里有一道经典坑题:为什么更新数据库之后是删缓存,而不是更新缓存?原因是更新缓存这个操作有并发时序问题。假设线程A和线程B同时写同一份数据,A先更新数据库为值1,B后更新数据库为值2,但B的Redis更新可能比A早完成,最终缓存里留的是旧值1,数据库是2,不一致了。而删除缓存的话,下次读取时自然会从数据库拉最新值回填,最终一致性能得到保证。

那能不能先删缓存再更新数据库?也不行。比如线程A删除缓存后还没更新数据库,线程B读缓存发现没有,就去数据库读到了旧值,然后回填缓存。等A更新完数据库,缓存里已经是B填的旧值了,数据不一致。所以Cache Aside的标准顺序,先更新库再删缓存,本质是尽量减少并发窗口。

6.2 延迟双删与Binlog订阅方案

先更新库再删缓存也有问题:删除缓存这个动作如果失败了,或者删除后恰好有线程把旧值回填了缓存,一样会不一致。延迟双删引入了一个sleep时间,流程是:先删除缓存,再更新数据库,等待一段时间,再次删除缓存。这个等待时间的目的是让并发读请求的“旧值回填缓存”动作在第二次删除前完成。

但延迟双删终究是一个概率性的解决方案,sleep设多少很难把握,太高影响性能,太低又起不到效果。更可靠的工程做法是订阅数据库的Binlog日志,比如通过Canal把MySQL的Binlog变更解析出来,再通过消息队列异步删除对应的缓存。这样做的好处是业务代码里不用手动删缓存,删缓存的动作和生产数据变更解耦,即使删失败了,也可以走MQ的重试机制再删一次。

面试时如果能把演进过程讲清楚,从Cache Aside到双删,再到Binlog订阅加MQ异步删除重试,展现出对一致性问题的完整思考链路,这道题基本就满分了。不过也要承认,没有绝对的一致性方案,最终选择的都是性能与一致性之间的平衡,这套平衡思路才是面试官最想看到的。

最后再分享一个小技巧,回答Redis面试题时别急着背答案,先想清楚面试官问这个问题的底层意图是什么。比如问数据类型,考察你对Redis存储结构的理解程度;问缓存穿透,考察你面对异常流量的防御思维;问分布式锁,考察你在分布式架构下的并发处理能力。把意图理清了,答案自然有层次。我面试这么多人,能给出有层次答案的候选人,最终几乎都拿到了offer。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询