资讯详情

资讯详情

Redis面试题全解析:从数据类型到分布式锁,原理与实战

“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如果元素少且值小会用listpackRedis 7.0之前是ziplist来节省内存当元素数量超过阈值时才转为hashtableZSet在数据量小的时候也用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到Redisson4.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存只有几十KBJDK序列化后能到几百KB内存存储成本直接翻好几倍。更坑的是JDK序列化还有类版本兼容问题。如果你修改了实体类的字段旧的缓存数据反序列化时会直接报错老缓存全部失效就得清掉重来。所以实际项目中我基本只用JSON类序列化Jackson或Fastjson都行存出来可读性好跨语言也没问题。真正有性能要求的场景可以考虑Protobuf这类二进制序列化但需要维护proto文件成本高一些。面试时如果被问到“Redis序列化应该注意什么”回答方向大概是考虑可读性和体积、考虑跨语言需求、考虑类结构变化后的兼容性。如果能再补一句“JDK序列化会把类元数据一起存进去导致空间浪费”基本就稳了。6. 缓存一致性业务里最头疼的延伸题6.1 Cache Aside模式为什么先更新数据库再删缓存缓存与数据库的一致性问题一般会在面试末尾被抛出来用来考察候选人解决实际问题的思路。最经典的模式是Cache Aside旁路缓存读的时候先读缓存没有就读数据库然后回填缓存写的时候更新数据库然后删除缓存。这里有一道经典坑题为什么更新数据库之后是删缓存而不是更新缓存原因是更新缓存这个操作有并发时序问题。假设线程A和线程B同时写同一份数据A先更新数据库为值1B后更新数据库为值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。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →