
这周连着面了好几场后端岗位几乎每一场我都会问Redis相关的问题。“缓存穿透怎么防”“分布式锁有没有遇到过坑”“缓存和数据库的一致性怎么保证”这三连问基本是后端面试的保留曲目。这一期常见面试题精讲我单独把缓存这块拎出来聊是因为我发现在这些问题上能完完整整讲清楚“为什么”的候选人真的不多。大部分人背过八股能说出布隆过滤器、Redisson、延迟双删这些名词但一旦被追问“误判率怎么算”“看门狗续期失败怎么办”“延迟双删到底延迟多久”就开始支支吾吾了。这一期的定位不光是给准备面试的人看的平时写业务代码、维护线上系统的人把这几个问题的思路理清楚对排查线上故障也很有帮助。我会按照我自己面试候选人的思路来展开先讲面试官想听到什么再逐个拆解缓存穿透、分布式锁、缓存一致性和热点Key这几类高频问题最后补充一些我在实际项目中踩过的坑和收尾技巧。1. 面试官问缓存相关面试题真正在考察的是什么很多候选人以为面试官问缓存就是在考“背答案”其实不是。缓存问题之所以高频是因为它背后关联着三块能力对基础组件的理解深度、对并发场景的敏感度、以及线上故障处理的经验。面试官通过一个缓存问题通常能快速判断出你的水平处在哪个层级。1.1 三个层次概念、原理、工程我一般把面试回答分为三个层次。第一个层次是概念层你能说出缓存穿透、击穿、雪崩的定义知道布隆过滤器、互斥锁、随机过期时间这些名词这是及格线。第二个层次是原理层你能解释清楚布隆过滤器为什么会有误判、Redis分布式锁为什么必须用Lua脚本释放、延迟双删为什么能解决缓存不一致这说明你对组件底层有理解。第三个层次是工程层你能结合自己的线上业务说明白“我为什么选择这种方案”“这个方案在极端情况下会出什么问题”“出了问题我怎么监控和降级”这才是面试官最想听到的。很多候选人停留在第一个层次所以面试官会觉得“这个人背过题”。举个我常问的例子如果有人答“缓存穿透用布隆过滤器解决”我下一句一定会问“布隆过滤器会有误判误判率怎么估算你觉得误判之后会发生什么”能接住这个问题的人才算是真正理解了这个方案。1.2 为什么缓存题能考察出这么多东西缓存是几乎所有后端系统都绕不开的组件。读多写少的业务要加缓存高并发接口要加缓存分布式系统的性能瓶颈往往也出在缓存上。所以面试官问缓存题本质上是在考察你对“读写链路”的理解是否完整。一条典型的请求链路是客户端请求到网关网关到业务服务业务服务先查Redis没命中再查数据库。这里面涉及网络开销、序列化开销、线程池占用、数据库连接池占用任何一个环节出了问题都可能产生雪崩效应。能把这套链路讲明白的候选人说明他不仅会用Redis还知道Redis在整个系统中的位置。这一期我就围绕这个读写链路把最常见的几类面试题拆开来讲。2. 缓存穿透、击穿、雪崩背答案的人很多讲清取舍的人很少缓存三大问题是面试必考题但大多数候选人的回答都是“穿透用布隆过滤器击穿用互斥锁雪崩用随机过期时间”。这个答案没错但太标准了没有信息量。这一节我来拆一下每个方案背后的原理和边界。2.1 缓存穿透布隆过滤器是标准答案但误判率怎么算穿透的本质是“请求的数据在缓存和数据库里都不存在”导致每次请求都直接打到数据库。攻击者可以利用这个特点故意请求一堆不存在的ID把数据库打垮。防穿透的常见方案有三种参数校验、缓存空值、布隆过滤器。参数校验能挡住格式明显不合法的请求比如负数ID、超长字符串缓存空值是指在查不到数据时也把空结果缓存起来设置较短的过期时间比如60秒布隆过滤器则是把所有可能存在的数据ID提前载入过滤器请求来了先判断ID是否存在不存在直接返回。布隆过滤器的原理是用多个哈希函数把一个元素映射到位数组的多个位置全部置为1。查询时检查这几个位置是否都是1如果有一个是0说明元素一定不存在如果全是1只能说可能存在。这个“可能存在”就是误判的来源。面试中如果能说出误判率的估算公式会非常加分。假设位数组长度为m哈希函数个数为k已插入元素个数为n那么误判率大约为[ p \approx (1 - e^{-kn/m})^k ]最优的哈希函数个数 k 约等于 (m/n) * ln2。举个例子如果我们要承载1000万个ID希望误判率控制在1%左右那么位数组长度大约需要 1000万 * 1.44 * log2(1/0.01) 大概 9600万 bit也就是不到12MB。这组数字在面试里讲出来面试官会明显眼神一亮。但要注意布隆过滤器有一个硬伤它不支持删除。如果业务里有ID被删除的场景误判率会随着数据量增长而上升。这时候可以改用布谷鸟过滤器但布谷鸟过滤器在极端情况下会陷入循环插入复杂度高不少。所以面试中不要只抛名词要能把取舍讲出来。2.2 缓存击穿互斥锁和逻辑过期二选一的标准是什么击穿和穿透的区别在于击穿是“缓存里有这个Key但Key在某个瞬间过期了刚好有大量请求涌进来”于是这些请求同时打到数据库导致数据库压力瞬间飙升。解决击穿的常用方案是互斥锁。查缓存没命中时先获取分布式锁拿到锁的线程去查数据库并回填缓存其他线程在锁外面等待然后直接读缓存。这个方案的优点是强一致缺点是会阻塞一部分请求极端情况下如果数据库很慢锁等待时间会很长。另一个方案是逻辑过期。我们在缓存Value里额外存一个过期时间字段比如 set key data:123, expireAt 当前时间60s。读缓存时发现逻辑过期了不直接删除Key而是先尝试获取一个互斥锁拿到锁的线程去后台重建缓存其他线程继续返回旧数据。这个方案的优点是不阻塞请求缺点是会有一段时间读到旧数据属于最终一致。面试中怎么答才能出彩我会说如果业务对一致性要求高比如库存、订单状态这种用互斥锁如果业务能接受短暂读到旧数据比如用户昵称、文章详情这种用逻辑过期。另外逻辑过期方案里一定要小心一个坑重建缓存的线程如果异常退出下一次请求会继续重建需要加一个重建次数上限或者报警机制。2.3 缓存雪崩随机过期时间解决不了所有雪崩雪崩的常见原因是“大量Key在同一时间窗口过期”。为什么会出现这种情况比如批量导入数据时设置了相同的过期时间或者上线定时任务统一刷缓存。解决方案是在设置过期时间时加一个随机数比如基础过期时间60秒再随机加0到10秒让Key的过期时间错开。但雪崩还有另一个场景不是Key同时过期而是Redis实例本身挂掉了。这种情况下随机过期时间没有任何作用。正确的应对方式是Redis高可用部署主从哨兵或集群模式、接口层做降级处理、本地缓存做兜底。降级的含义是Redis不可用时直接返回默认值或旧数据而不是把所有请求放行到数据库。这里我要强调一个容易忽略的问题。很多候选人会说“我用多级缓存解决雪崩”但如果一级缓存和二级缓存都存的是同一份数据并且都在同一时刻失效那多级缓存也救不了你。真正的多级缓存每一级都应该有自己的兜底逻辑比如本地缓存过期时间要设置得比Redis更长这样才能形成保护。2.4 三兄弟对比一张表记住它们的区别问题数据是否存在发生时机影响范围核心应对方案缓存穿透缓存和DB都不存在持续发生每次请求都打DB参数校验、缓存空值、布隆过滤器缓存击穿数据存在但热点Key过期单个Key过期瞬间单点热点请求打DB互斥锁、逻辑过期缓存雪崩大量数据存在大量Key同时过期/实例宕机大面积请求打DB过期时间随机化、高可用、降级3. Redis分布式锁从setnx到Redlock答到哪一步才算稳分布式锁是缓存相关面试题里最容易翻车的一题。因为很多候选人只知道“用setnx加锁”但一旦被问到“锁过期了业务还没执行完怎么办”“释放锁的时候怎么保证安全”就不知道该怎么接了。3.1 第一版实现setnx expire 为什么是错的最早很多代码是这样写的Boolean locked redisTemplate.opsForValue().setIfAbsent(lock:order, uuid); if (locked) { redisTemplate.expire(lock:order, 30, TimeUnit.SECONDS); }先执行setnx再单独执行expire。这个写法有一个致命问题如果setnx成功之后、expire执行之前进程崩了锁就没有过期时间会变成永久锁其他线程永远拿不到锁。正确做法是使用Redis提供的原子命令Boolean locked redisTemplate.opsForValue() .setIfAbsent(lock:order, uuid, 30, TimeUnit.SECONDS);setnx和expire合并成一个原子操作要么都成功要么都失败。Java的StringRedisTemplate底层会翻译成SET key value NX EX也就是Redis的SET命令加上NX和EX参数保证原子性。这是分布式锁最基本的规范写错就会被面试官直接判负。锁的Value为什么要用UUID因为释放锁的时候要验证“锁是不是自己的”。如果不验证可能出现这种情况线程A拿到锁后执行了很久锁已经自动过期线程B拿到了同一个Key的锁此时线程A执行完释放锁把线程B的锁删掉了。用UUID作为Value释放前先比较一下就能避免这种误删。3.2 释放锁为什么必须用Lua脚本保证原子性释放锁的逻辑看起来很简单先比较Value相等就删除。但“比较”和“删除”是两步操作如果不加原子性中间会出问题。比如线程A比较Value发现自己持有锁但还没来得及删除锁过期了线程B拿到锁然后线程A执行删除把B的锁删了。解决方法是把“比较删除”放进Lua脚本里执行if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 endRedis保证Lua脚本是原子执行的脚本执行过程中不会插入其他命令。这段代码是面试必背而且面试官一定会问“为什么不能分开执行”答案就落在这个两步操作的时间窗口上。3.3 Redisson的看门狗机制是怎么回事了解了基本实现之后下一个问题通常是锁过期时间设置多长设置30秒业务执行超过30秒怎么办设置10分钟万一线程崩了锁要10分钟后才释放影响后续请求。Redisson的解决思路是“看门狗”自动续期。Redisson默认leaseTime是30秒拿到锁之后会启动一个后台定时任务每隔10秒leaseTime的三分之一检查一次锁是否还在如果还在就把过期时间重置为30秒。这样业务执行多久锁就持续多久直到线程主动释放或线程崩溃。面试中值得提的一个细节是如果你手动指定了leaseTime比如setLock(100, TimeUnit.SECONDS)Redisson就不会启动看门狗续期因为此时你明确指定了锁的最大持有时间。所以不要既指定了超时时间又指望看门狗帮你续期这两者是互斥的。3.4 Redlock到底能不能用别在面试里站死队Redlock是Redis作者提出的分布式锁增强方案思路是向多个独立的Redis节点申请锁超过半数节点加锁成功才算获取锁成功。这个方案在分布式系统领域争议很大Martin Kleppmann专门写过文章批评它主要论点是Redlock底层依赖系统时间如果某个节点发生时钟跳跃锁的安全性可能被破坏。面试中遇到Redlock的问题千万不要直接说“Redlock是错的不能用”或者“Redlock绝对可靠”。更好的回答是Redlock在理论上有时钟跳跃的风险但实际业务中如果锁的持有时间很短比如毫秒级这个风险概率极低。绝大多数业务场景单机Redis配合Redisson看门狗已经足够。如果对安全性要求极高团队也有能力承担运维成本可以引入Redlock但一定要明白它解决的是“Redis主从切换导致锁丢失”的问题而不是所有分布式锁问题。4. 缓存一致性先删缓存还是先更新数据库这道送命题怎么答缓存一致性几乎每场面试都会问到。说实话这道题没有一个完美的答案因为缓存和数据库的一致性天然是最终一致的面试官想看到的是候选人对时间窗口的理解以及如何把这个窗口缩到最小。4.1 一致性问题的本质读旧数据这件事能不能避免先想清楚一个前提为什么要加缓存就是因为读远多于写我们希望大多数读请求从缓存里拿数据从而减轻数据库压力。但写入的时候缓存没法感知数据库的变化所以必然存在一个“数据库已经更新、缓存还是旧值”的时间窗口。这个窗口能完全消除吗能但代价非常大。比如在读写链路里引入分布式事务先更新数据库再更新缓存两者必须同时成功否则回滚。但分布式事务的性能损耗非常大绝大多数业务都扛不住。所以现实中我们都在做妥协接受一个足够短的不一致窗口或者通过补偿机制把窗口关闭。想明白这一点之后你再回答接下来的问题思路就不会乱。4.2 Cache Aside 模式里的并发时序问题Cache Aside旁路缓存是最常用的读写模式读的时候先读缓存未命中就查库并回填缓存写的时候更新数据库同时删除缓存。这里最大的争议就是“先删缓存还是先更新数据库”。先删缓存再更新数据库会有这个问题线程A删除缓存线程B此时读到缓存未命中去数据库里读取旧值然后回填缓存。紧接着线程A更新数据库。结果是缓存里存了旧值且后续所有请求都命中旧缓存直到这个Key再次被删除。这就是长期不一致的来源。先更新数据库再删缓存表面上问题小一些但依然存在时间窗口线程A更新数据库线程B读取的是旧缓存值。等到线程A删除缓存后后续请求才会读到新数据。这个不一致窗口就是“数据库更新成功”到“缓存删除成功”之间的那段间隔通常只有几毫秒。但如果线程A删除缓存失败呢缓存会一直保持旧值。所以需要在删除失败时重试或者定期校准缓存。相比之下先更新数据库再删缓存的方案更安全因为不一致窗口短而且可以配合重试。这也是很多团队默认采用的方式。4.3 延迟双删什么时候用延迟多久才合理延迟双删的做法是先删除缓存再更新数据库隔一段时间后再删除一次缓存。这么做的目的是解决4.2里的场景——线程B在第一次删除之后、更新数据库之前把旧值回填到缓存导致缓存长期不一致。第二次删除可以把这条旧缓存删掉强制后续请求重新加载。但“延迟一段时间”到底是多久很多文章直接写“延迟500毫秒”这是不对的。正确的做法是这个延迟时间要大于一次业务读请求的耗时确保在你第二次删除之前并发读的请求已经把旧值回填完成了。比如你的业务读耗时平均是50毫秒P99是200毫秒那么延迟至少需要300毫秒甚至更久。如果中间涉及网络抖动最好再往上加。延迟双删的缺点也很明显第二次删除是异步的如果异步任务失败了缓存还是会残留旧值。所以延迟双删只是一个补偿手段不能把它当成银弹。4.4 加分回答用binlog订阅方案实现最终一致性大厂里更常见的做法是让应用不关心缓存删除而是由数据库变更来驱动。以MySQL为例Canal或者Flink CDC可以订阅MySQL的binlog当检测到某张表的记录变更时自动删除对应缓存。这个方案的好处是显而易见的应用代码只需要更新数据库缓存失效的逻辑被下沉到一个独立的同步链路里更新数据库和删除缓存之间不再有耦合。binlog是顺序变更的天然有序避免并发场景下“先更新的数据反而晚删除缓存”这类乱序问题。面试时提到这个方案可以再加一句binlog订阅方案最终能保证一致性但不能保证实时性因为binlog同步有延迟通常在毫秒级到秒级。对一致性要求极其严格的场景比如支付、库存预扣建议直接在数据库层面做不要依赖缓存。5. 热点Key和大Key线上实战题答好这题基本能定级如果说前几题还在考基础那么热点Key和大Key就是在考一线经验了。没有真正处理过线上问题的人是答不好这题的。面试官问到这里基本就是想探一探你有没有在真实流量下干过活。5.1 热点Key怎么发现热点Key通常表现为某个Key的QPS异常高比如一个商品的详情Key每秒有几十万次请求。发现的方式主要有几种。第一种是Redis的monitor命令它能实时打印所有命令但线上环境一般不敢用因为monitor会显著降低Redis性能。第二种是Redis 4.0以后提供的redis-cli --hotkeys它会扫描整个Redis统计每个Key的访问频率找出热点Key。但扫全库也有影响一般建议在业务低峰期执行。第三种是客户端埋点在应用侧统计每个Key的访问次数超过阈值就上报到监控平台。这是最推荐的做法因为监控粒度更细也能按业务维度定位。我个人比较倾向于第三种。原因很简单monitor和--hotkeys都站在Redis的角度去“看”热点但客户端埋点能告诉你“是哪个业务、哪个接口在打这个Key”排查起来更快。5.2 热点Key治理从打散Key到本地缓存发现热点Key之后治理方案有四类。第一类是Key打散。比如商品详情Key是product:123可以在Key后面加随机后缀拆成product:123_1、product:123_2等几十个Key均匀分布在Redis集群的不同节点上让读写压力分散。缺点是同一个商品的数据被复制了多份更新数据时需要同时更新这些分散Key一致性处理变得麻烦。第二类是本地缓存。JVM进程内缓存一份热点数据请求先查本地缓存不经过Redis。这个方案性价比很高也是我处理秒杀场景的常用手段。但有个前提只有读多写少的数据才适合放本地缓存如果数据频繁变更本地缓存会带来严重的缓存一致性问题。第三类是增加副本。在Redis集群里对热点Key做多副本读写读请求分散到多个副本上。这个方案对Redis Cluster来说需要应用层配合实现起来有一定成本。第四类是限流和降级。热点Key如果超出了系统的处理能力该限流还是得限流避免热点演变成整个集群的灾难。5.3 大Key的识别与拆分不要等报警了才处理大Key包含三类String类型的value特别大比如一个字符串有几MBHash集合的field特别多比如上百万个fieldZSet成员特别多比如上千万个成员。大Key的危害主要有三个第一Redis是单线程处理命令的读取一个几MB的String或者遍历一个百万field的Hash会阻塞Redis主线程导致其他正常命令排队变慢。第二在集群模式下大Key会导致数据倾斜某个节点内存占用特别高其他节点却很空闲。第三删除大Key时如果直接DEL也会阻塞主线程DEL一个几GB的Key可能导致Redis卡顿好几秒。如何识别大Key用redis-cli --bigkeys它会扫描全库统计每个Key的元素个数和大小。注意--bigkeys在扫描过程中会持续访问大量Key建议在低峰期执行。大Key怎么处理String类型的大Value可以先压缩再缓存或者拆分成多个小Key分片存储Hash或ZSet可以按业务维度进行分片比如按用户ID取模拆成多个小Key每次只读一个分片过期时间也要拆开避免所有分片同时过期。删除大Key要用unlink命令替代delunlink会在后台释放内存不阻塞主线程。6. 除了标准答案我还想补充的几个实操细节技术题回答完了最后分享几个我在团队里经常强调的实操细节。这些细节不会直接出现在面试题里但能体现你是不是一个真正在线上“扛过枪”的人。6.1 说性能提升时要给出对比数据面试中我经常遇到候选人说“加了缓存之后性能提升很明显”我追问“明显到什么程度”就答不出来了。正确表述应该是加缓存之前接口P99是120毫秒数据库QPS峰值8000加缓存之后P99降到20毫秒数据库QPS峰值降到2000。数据不会说谎这也是你对自己优化能力最好的证明。6.2 缓存层一定要有监控和报警没有监控的缓存就是一颗定时炸弹。线上Redis的命中率怎么监控客户端要统计“读缓存成功次数”和“读缓存总次数”命中率 命中次数 / 总次数。如果命中率突然从95%掉到60%大概率是Key的过期策略出了问题或者缓存被大量清空。延迟双删的执行率、锁的等待时间、看门狗续期失败次数这些都值得做成监控指标。6.3 回答面试问题时主动讲清楚边界面试官最怕的就是候选人背了一堆方案却不知道每个方案的适用边界。比如布隆过滤器不能删除互斥锁会阻塞请求延迟双删要估算业务耗时本地缓存要处理一致性。你主动把这些边界讲出来面试官会认为你不只是“用过”而是“想过”。这一期的面试题精讲到这里就差不多了。我在实际面试里有个习惯如果候选人能把热点Key的发现方式分个优劣把锁的续期原理讲明白把缓存不一致的窗口分析清楚我基本会给他一个偏上的技术评价。这些能力不是靠背题能背出来的还是要回到线上一点点积累。如果你正在准备面试建议把这些题当成一个“复盘提纲”结合自己的项目把每一步的取舍和数据都整理出来效果会比单纯记答案好很多。