后端开发做久了,Redis 绕不开一个灵魂拷问:缓存穿透、缓存击穿、缓存雪崩,到底怎么区分,怎么解决。我最早听到这三个词,是在一次线上事故复盘。凌晨流量一上来,数据库连接数瞬间被打满,接口超时一大片,最后定位到的原因很简单——一批恶意请求在用不存在的商品 ID 疯狂打接口,Redis 里查不到,MySQL 被拖下水,整个商品服务直接打挂。那次之后我把这三个问题从前到后研究了个遍,网上资料说得都挺全,但真正落到代码和线上排查时,还是有不少细节值得捋一捋。今天这篇就专门聊缓存篇最核心的三大难题:穿透、击穿、雪崩的原理、解决方案,以及我在实际项目里的选型取舍。不管你是准备面试,还是刚接手高并发项目,这套思路都能直接拿过去用。
1. 三大缓存问题:先分清谁是谁
很多人在刚接触这三个概念时会绕晕,因为名字太像了,而且都和“缓存失效”沾边。我的经验是:先把场景区分清楚,后面的方案自然就记住了。
1.1 穿透、击穿、雪崩,名字相近但死法不同
用生活里的场景打个比方。缓存穿透,就像你家楼下有张门禁表,上面登记了住户名单,结果总有骗子报一个并不存在的住户名字,保安每次都认真去楼里找一圈才告诉你没这人,骗子多了,保安累死。落到系统里,就是“查询的 Key 在缓存和数据库里都不存在”,每次请求都穿过了缓存这道防线,直接打到数据库。
缓存击穿,场景更具体一些。某个热点 Key 正被人疯狂访问,比如秒杀商品、微博热搜词条,结果这个 Key 在某个瞬间过期了,于是大量并发请求同时发现缓存没有,全部冲进数据库去查。数据库本来扛得住单次查询,但扛不住几万个查询同时涌入。
缓存雪崩则是范围更大的灾难。它不是某一个 Key 过期,而是一大批 Key 在同一时间段内集中失效。比如你用定时任务给所有商品设置 30 分钟过期时间,那么每隔 30 分钟就会有一波“集体过期”,如果这个时间点恰好赶上了流量高峰,数据库会被瞬间打满。更极端的场景是 Redis 服务本身宕机,连缓存都不可用了,全部流量直接打到下游存储。
1.2 一张表看懂三者的核心差异
我面试候选人的时候,经常让他们用一句话说清三者的区别。如果只谈模糊印象,说明没吃透。这里用表格直接对比:
| 维度 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| 故障原因 | 查询的数据根本不存在 | 单个热点 Key 突然过期 | 大量 Key 集中过期或缓存服务不可用 |
| 影响范围 | 通常是无序的单个请求 | 集中在某一个高热度 Key | 大面积数据同时失效 |
| 数据库表现 | QPS 被不存在的数据打高 | 单 Key 对应的 DB 查询并发暴涨 | 整体 DB 负载瞬间飙升 |
| 本质 | 缓存没起到拦截作用 | 缓存重建缺少并发控制 | 缓存失效时间设计不合理 |
| 解决重心 | 拦截无效请求 | 控制并发重建 | 打散失效时间 + 服务降级 |
这个顺序也正好对应了从“缓存设计”到“系统架构”的递进。穿透考验的是你对非法请求的识别能力,击穿考验的是并发控制手段,雪崩考验的是整体容灾思路。下面逐个展开讲。
2. 缓存穿透:请求根本不存在的 Key
穿透是最常见、也最容易复现的问题。很多团队一开始没在意,等线上出事才发现,原来空 Key 也会打死数据库。
2.1 穿透的根因与危害
穿透的根源是:缓存没有命中,数据库也没有这个数据,于是这个“查无此物”的请求每次都完整地走了一遍主链路。
正常业务里这种情况不致命,因为量少。但两类场景会被放大。一类是恶意攻击,外部调用方想拖垮你的服务,就专门构造不存在的 ID 来刷接口,比如订单号、手机号这种规律性强的字段;另一类是业务逻辑存在缺陷,例如用户上传了一个错误的 ID,或前端缓存了已删除的商品信息,导致请求持续打到后端。
危害很明显:数据库连接是稀缺资源,每一条 SQL 都要占用连接、CPU、磁盘 IO。缓存穿透时,Redis 查不到并不意味着 MySQL 无压力,恰恰相反,因为 Redis 只是快速放行,MySQL 才是真正被轰炸的对象。如果数据库连接池或者慢查询有瓶颈,一个接口被刷就可能连累整个应用。
2.2 能拦就拦:接口层的参数校验
面对穿透问题,我的排查顺序从来都是:第一步先看参数校验中间层有没有漏。
比如商品 ID 有固定规则,如纯数字、最小 1、最大 100 万,那就应该在 Controller 层或网关层直接拦截掉非法的 ID。有些系统还会给 ID 加上签名、hash 或时间戳校验,根本不给伪造请求进入服务的机会。参数校验的好处是零成本、零延迟,还能顺带防止不少安全攻击。
但真正的有效做法还在后面,因为恶意攻击者可以伪造“合法格式”的 ID,比如 99999999 这种位数正确但实际不存在的值。所以参数校验只是一道前置过滤,不能作为穿透的兜底方案。
2.3 常规兜底:缓存空值
参数校验挡不住的情况下,业界最常规的做法是把空值也缓存起来。写过 Redis 代码的同学应该很熟悉这类逻辑:
public String getProduct(Long id) { String redisKey = "product:" + id; String value = redisTemplate.opsForValue().get(redisKey); if (value != null) { return value; } // 缓存中没有,去数据库查 String product = queryDbById(id); if (product == null) { // 数据库也没有,缓存一个空值,并设置较短过期时间 redisTemplate.opsForValue().set(redisKey, "", Duration.ofMinutes(3)); return null; } redisTemplate.opsForValue().set(redisKey, product, Duration.ofMinutes(30)); return product; }这段代码看起来简单,但有几个细节值得说道。第一,空值的 TTL 一定不能太长,建议 3~5 分钟,否则大量不存在的 Key 会占满 Redis 内存;第二,要判断“整个商品数据都不存在”还是“商品存在但内容为空”,不要混淆,不然会把有效数据误判成空;第三,如果系统里空 Key 特别多,可以考虑将空 Key 单独放一个 Redis 逻辑库或一个专门前缀,方便后续清理。
缓存空值适合绝大多数中小团队,优点是实现简单、几乎不改架构,缺点也明显:如果攻击方构造的 ID 无规律且数量巨大,空值缓存会持续堆积,内存占用不可控。所以它处理的是“低频空查询”,扛不住“恶意流量轰炸”。
2.4 高并发下的布隆过滤器
当穿透流量大到一定程度,就需要换一种思路:从源头判断某个 Key 到底存不存在。布隆过滤器就是为这个场景设计的。
布隆过滤器的原理可以用一句话概括:一个很长的位数组,加上多个哈希函数。插入数据时,把数据经过多个哈希函数算出多个位置,把这些位置置为 1;查询时,只要发现任何一个位置是 0,就说明这个数据一定不存在;如果所有位置都是 1,说明数据可能存在。
你可能会问:为什么不是“一定存在”?因为多个数据可能哈希到相同位置,产生冲突,导致一个不存在的 Key 把所有位置都撞成 1,这个就是误判率。布隆过滤器允许这种误判,但它的优点在于绝对不会漏判——判断为不存在的数据是真的不存在。
工程上最好用的方式是使用 Redisson 自带的布隆过滤器,或者 Redis 的 RedisBloom 模块。如果项目里已经有 Redisson,用起来很顺手:
RBloomFilter<String> bloomFilter = redissonClient .getBloomFilter("productBloomFilter"); // 预估数据量为100万,期望误判率为1% bloomFilter.tryInit(1000000L, 0.01); // 项目启动或商品ID变更时,把有效的商品ID都初始化进过滤器 for (Long id : allValidProductIds) { bloomFilter.add(String.valueOf(id)); } // 查询接口中,先用布隆过滤器判断 public String getProduct(Long id) { if (!bloomFilter.contains(String.valueOf(id))) { // 这个ID肯定不存在,直接返回 return null; } // 后面走正常的 Redis -> DB 查询逻辑 }布隆过滤器最怕两件事:一是初始化阶段没把全量数据灌进去,二是数据真删除了但过滤器里仍然保留着旧 ID,导致这部分删除数据始终能穿透到数据库。通常的做法是布隆过滤器只负责“大概率拦截”,同时配合空值缓存做二次兜底,既挡掉大部分攻击流量,又把误判产生的少量穿透用短 TTL 的空值给抚平。
2.5 我实际项目里的选型
我接手过的几个项目里,一开始都用的缓存空值方案,原因是简单、代码量小。后来有一个项目被黑产盯上,对方专门用递增 ID 扫我们商品库,空值缓存积累了一个量级。我们最后切换成布隆过滤器加缓存空值的组合,效果非常明显,数据库 QPS 降了一个数量级。
如果你在评估方案,我的建议是:业务量不大、没有明显恶意流量时,空值缓存完全够用;如果存在接口被刷的风险,或者你们已经遇到过一次穿透事故,那直接上布隆过滤器,别犹豫。还有一个小细节,布隆过滤器要预留足够的容量,否则随着数据增长,误判率会迅速上升,过滤器形同虚设。
3. 缓存击穿:热点 Key 过期的那几毫秒
如果说穿透是把“不存在”的数据拦截在缓存之外,那么击穿面对的则是“存在但缓存刚好没电”的瞬间。问题是,这个瞬间往往就是流量最高的瞬间。
3.1 击穿的本质和触发条件
击穿发生的三要素:某个 Key 是热点数据、这个 Key 有固定过期时间、在过期那一刻有大量并发请求。
典型的场景是微博热搜。一个词条冲上热搜,短时间内被读取几十万次,缓存里明明有数据,突然到了 TTL 时间,缓存过期了。此时如果有几百个线程同时去数据库查询该词条的信息,数据库单表查询确实很快,但连接数和并发线程数会瞬间飙升,慢查询和连接池耗尽随之而来。
很多人把击穿和穿透混淆,区别就在这里:击穿的数据是真实存在的数据,只是缓存恰好在高并发时刻失效了;穿透的数据是根本不存在的 Key。
3.2 互斥锁:让数据库只被请求一次
击穿的核心矛盾是:多个线程同时发现缓存为空,于是同时去数据库查询,而这些查询是重复的。最好的办法是在“发现缓存为空”和“去数据库查询”之间加一把锁,保证同一时刻只有一个线程去查数据库并回填缓存,其他线程等待。
Redis 里的 setnx 就是天然实现分布式锁的工具,Sring 项目集成 RedisTemplate 后可以这样写:
public String getProduct(Long id) throws InterruptedException { String redisKey = "product:" + id; String value = redisTemplate.opsForValue().get(redisKey); if (value != null) { return value; } // 尝试获取分布式锁,30秒后自动过期 String lockKey = "lock:product:" + id; Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofSeconds(30)); if (!locked) { // 没拿到锁,说明别的线程正在重建缓存,休眠后重试 Thread.sleep(50); return getProduct(id); } try { // 拿到锁后二次检查,防止拿到锁之前缓存已经被重建 value = redisTemplate.opsForValue().get(redisKey); if (value != null) { return value; } // 真正回源数据库查询 value = queryDbById(id); redisTemplate.opsForValue().set(redisKey, value, Duration.ofMinutes(30)); return value; } finally { // 释放锁 redisTemplate.delete(lockKey); } }这段代码有几个注意点。第一,拿到锁后一定要做“二次检查”,因为可能在等锁的过程中,第一个线程已经把缓存回填了。第二,锁的过期时间要大于“数据库查询 + 回填缓存”的总耗时,如果查询很慢,锁提前过期,后面线程又会涌进来,击穿问题就退化成穿透问题。第三,释放锁时要校验是不是自己加的锁,防止自己的锁被别人删掉。我的习惯是 setnx 的 value 放一个 UUID,释放前对比一下再删除。
互斥锁方案的优点是强一致性:缓存一定是在数据库查到最新值后回填,不会出现旧数据;缺点是集中式的锁机制在高并发下可能成为性能瓶颈,而且如果热点特别放大,大量线程都在等锁。
3.3 逻辑过期:缓存里存的不只是数据
还有一种用得越来越多的方案,叫逻辑过期。它不在 Redis 层面设置 TTL,而是把过期时间作为一个字段存进缓存对象,由业务代码自己判断这个缓存是否过期。
大致思路是这样:
public class CacheData<T> { private T data; // 真正的业务数据 private long expireTime; // 逻辑过期时间戳 }查询的时候:
public String getProduct(Long id) { String redisKey = "product:" + id; CacheData<String> cacheData = getFromRedis(redisKey); if (cacheData == null) { // 不是逻辑过期,而是缓存彻底不存在,说明Key没建过或被手动删除 String value = queryDbById(id); saveToRedis(redisKey, value, 30 * 60 * 1000L); return value; } if (cacheData.getExpireTime() > System.currentTimeMillis()) { // 逻辑上还没过期,直接返回 return cacheData.getData(); } // 逻辑过期,这里有两个选择: // 1. 直接返回旧数据,同时异步更新缓存 // 2. 尝试获取锁,由持有锁的线程去更新缓存 executor.execute(() -> rebuildCache(id)); return cacheData.getData(); }逻辑过期最大的好处是可用性。缓存里的数据即使逻辑过期了,用户拿到的还是旧数据,不会出现缓存击穿导致数据库压力暴涨的情况。代价是会短暂读到过期数据,适合对一致性要求不算极端的场景,比如商品详情、用户资料、文章页。
实际开发中,逻辑过期常和“缓存写更新”配合。比如后台改了商品价格,先更新数据库,再主动刷新 Redis 里的缓存,把逻辑过期时间顺延。这样缓存中的数据理论上是新的,逻辑过期时间只作为一个兜底红线。
3.4 永不过期 + 异步刷新
有同学会问,干脆把热点 Key 的 TTL 设置成永不过期行不行?我告诉你,真实项目里很多人就是这么干的,但方法是“物理不过期,逻辑会过期”。
具体做法是:把 Redis 里的 Key 不设置 TTL,同时用定时任务或延迟消息定期去更新这个 Key。比如每 5 分钟自动刷一次热点数据,数据更新后主动推掉缓存。
这类方案的优点是彻底规避击穿问题,因为缓存永远存在,不会出现并发回源;缺点是热点数据更新的实时性依赖刷新任务的频率,如果刷新任务挂了,缓存内容就会一直旧下去。所以线上一般会加一层监控:预热任务失败时触发告警。
我见过最稳定的配置是“永不过期 + 双写 + 兜底刷新”。“双写”指收到数据变更消息后立即刷新缓存;“兜底刷新”指定时任务周期性地把数据库里的最新数据回填到缓存中。这个组合可以覆盖大多数业务场景。
3.5 三种方案怎么取舍
| 方案 | 一致性 | 可用性 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 互斥锁 | 强一致 | 一般 | 中 | 一致性要求高,缓存重建较快 |
| 逻辑过期 | 短暂不一致 | 高 | 中偏高 | 允许短时间读旧数据,高并发热点 |
| 永不过期 + 异步刷新 | 取决于刷新频率 | 最高 | 高 | 核心热点数据,数据变化不频繁 |
我的经验是:大部分项目首选互斥锁,因为它能保证“不漏过数据库”,而且代码审计容易过;当性能压测发现锁竞争激烈时,再考虑逻辑过期或永不过期方案。如果你在面试时被问到,最好把这些方案都讲一遍,然后结合项目场景给出自己的选择理由。
4. 缓存雪崩:大批 Key 在同一时刻失效
如果说击穿是“单兵作战”,雪崩就是“全面崩溃”。当天量的 Key 同时失效,数据库会在短时间内涌入平时几十倍的流量,大概率直接被打挂。
4.1 雪崩与击穿的分界线
很多初学者分不清击穿和雪崩,我这里给一个判断标准:看影响的 Key 的范围。击穿是单个热点 Key,雪崩是一大批 Key,甚至整个缓存服务不可用。
雪崩的产生通常有两个层面。一个是缓存层自身的设计问题:设置了固定过期时间,导致大量 Key 在同一时刻批量过期;或者定时任务在整点更新一批数据,引发了“集体失效”。另一个是基础设施层面的问题:Redis 集群宕机,连接全部失败,高并发系统里的缓存保护变成空谈,所有流量直冲数据库。
第二个层面已经超出了“缓存过期策略”的范围,属于高可用架构要解决的事,所以我在 4.4 会重点讲服务降级。
4.2 过期时间打散:最便宜的一招
不管雪崩怎么升级,最基础的手段永远是给 TTL 加上随机偏移量。这也是我入职任何项目最先检查的点。
看这段代码:
// 不推荐:每隔30分钟集中失效 int expire = 30 * 60; redisTemplate.opsForValue().set(redisKey, value, Duration.ofSeconds(expire)); // 推荐:TTL基础上加0~60秒的随机值 int expire = 30 * 60 + new Random().nextInt(60); redisTemplate.opsForValue().set(redisKey, value, Duration.ofSeconds(expire));原理很简单:让每个 Key 的过期时间不完全相同,把集中失效的时间点打散。不要小看这个随机值,它可以在几乎零成本的情况下让数据库的最高瞬时压力降一个量级。
如果需要多级缓存,这一招同样适用。比如“本地缓存 + Redis”组合,如果本地缓存是 5 分钟失效,Redis 是 30 分钟失效,也要注意不要让本地缓存全部在同一秒失效,否则会引发缓存穿透。业界有一种“双失效时间”策略:本地缓存设上限(比如 5 分钟),但每次取缓存时随机加一个 1~60 秒的慢启动时间,保证缓存不会团灭。
4.3 多级缓存:层层设防
只靠 Redis 一层,风险还是太大。更稳妥的做法是引入本地缓存(如 Caffeine、Guava Cache 或 Ehcache),让一部分请求只经过 JVM 内存,根本不去访问 Redis。
我的策略是两层缓存都检查:先查 Caffeine,再查 Redis,最后才是数据库。
// 本地缓存:最大5分钟,容量10万 Cache<String, String> hotCache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) .maximumSize(100_000) .build(); public String getProduct(Long id) { String key = "product:" + id; // 第一层:本地缓存 String localValue = hotCache.getIfPresent(key); if (localValue != null) { return localValue; } // 第二层:Redis String redisValue = redisTemplate.opsForValue().get(key); if (redisValue != null) { // 回填本地缓存 hotCache.put(key, redisValue); return redisValue; } // 第三层:数据库 String dbValue = queryDbById(id); redisTemplate.opsForValue().set(key, dbValue, Duration.ofMinutes(30)); hotCache.put(key, dbValue); return dbValue; }多级缓存带来的直接好处是:Redis 挂了之后,还有本地缓冲顶着,数据库不会被瞬间打穿。本地缓存的命中率可能不高,但对于热点数据来说,效果立竿见影。
需要注意的是,本地缓存和 Redis 之间会存在数据一致性问题。同一个应用实例里的本地缓存是独立的,数据更新时要双写:一个线程更新数据库,另一个线程同步清掉 Caffeine 里的 Key或重置 TTL。如果更新不及时,用户可能会读到旧数据。所以本地缓存更适合读多写少的场景。
4.4 限流、降级与熔断:保命的三件套
当雪崩真的发生,Redis 也好、多级缓存也好,都可能已经沦陷。这个时候最核心的任务不是追求数据完整,而是保证系统不挂,给用户一个可接受的响应。
这时候就要用上流量治理三板斧:限流、降级、熔断。我用阿里的 Sentinel 或者 Spring Cloud 体系的 Hystrix 都能实现,思路是一致的。
限流负责控制进入数据库的流量,比如只允许每秒 1000 个请求通过,超过的直接返回一个“系统繁忙”或兜底数据;熔断负责在下游数据库异常时快速失败,避免请求长时间占用线程池;降级则是主动放弃非核心功能,比如展示一个静态页或默认文案,保证核心链路可用。
最常见的做法是在缓存查询失败或超时后面加一个兜底方法:
public String getProductFallback(Long id) { // 当Redis不可用或查询超时时,返回本地预制的默认数据 // 或者从ES、HBase等其他存储读取,也可以直接返回null return localStaticMap.getOrDefault("defaultProduct", "商品信息加载失败"); }我见过不少事故,其实数据库并没有挂,只是流量太大导致连接池满了,然后慢查询堆积,最终把数据库拖死。提前加上限流和降级,能在雪崩初期就掐住最危险的流量入口。这是所有高并发系统最后一道防线,一定不能省。
4.5 缓存预热:大促前必修课
雪崩很多时候是“自己踩出来的坑”。比如大促开始前,开发同学往 Redis 里写入一批商品的 30 分钟缓存,结果大促一开始,这批缓存齐齐失效,数据库就遭殃了。
缓存预热的本质是:在流量高峰到来之前,把可能要访问的数据提前加载到缓存中,并设置好合理的过期时间。同时,要在预热阶段检查是否有“同一时间过期”的情况。
大型活动上线前,我的标准操作是这样的:
- 梳理活动期间可能被高频访问的 Key 的清单;
- 分批写入缓存,每一批的过期时间加一个随机偏移量;
- 使用脚本统计这些 Key 的过期时间分布,确保不会集中在同一秒;
- 在流量峰值前的 10 分钟再统一刷新一次热点数据;
- 容量预估后决定是否要提前扩容 Redis 集群。
这些动作看起来琐碎,但往往能避免一场事故。预热不是简单地把缓存填满,而是把“过期时间设计”和“流量预期”都考虑进去。
5. 线上排查实录与避坑指南
理论讲完,实操才是关键。很多同学背了一堆方案,但线上真出现问题,连是哪种缓存问题都判断不出来,更别提定位原因了。
5.1 出问题后怎么快速定位是哪一种
我一般会这样排查:
第一步,看监控指标。如果数据库 QPS 瞬间飙升,但 Redis 的 QPS 没有明显变化,说明大量请求没有走缓存逻辑或者穿透到了数据库。如果 Redis 的 QPS 也飙升,但 Redis 的 Key 数量没有同步增长,很可能是热点 Key 过期引发击穿。
第二步,看日志。穿透的日志特征是“同一类不存在的 Key 反复请求”,击穿表现为“某个热点 Key 短时间内出现大量回源日志”,雪崩则是“大面积 Key 的 miss 率突然拉高”。
第三步,看 Redis 慢日志和 Key 分布。用 redis-cli 执行 SLOWLOG GET 可以获取慢命令,这些慢命令往往能帮你找出是不是某些超大 Value 导致网络传输阻塞。再用 SCAN 命令统计过期 Key 的数量和时间分布,能快速确认是不是集体失效。
| 现象 | 可能原因 | 重点排查对象 |
|---|---|---|
| 数据库 QPS 高,Redis QPS 无明显波动 | 缓存穿透 | 参数校验、不存在的 Key 比例 |
| 某一个业务接口在固定时间点延迟飙升 | 缓存击穿 | 该接口对应的热点 Key 过期情况 |
| 大量接口同时超时,Redis 连接报错 | 缓存雪崩 | 过期时间策略、Redis 集群状态 |
5.2 最容易踩的两个坑
第一个坑是缓存重建耗时太长。互斥锁方案里,如果数据库查询需要 2 秒,而锁的过期时间设置成 1 秒,后面的请求就全跟着遭殃。我们曾有业务场景是商品详情接口需要聚合多个服务的数据,回填缓存可能要 500 毫秒,如果锁过期时间只设 300 毫秒,压测时直接废掉。经验值是:锁过期时间 = 正常回源耗时的 2 倍,并加一个 500 毫秒的 buffer。
第二个坑是忽略“有 Key 但 Value 为空”的情况。很多同学针对穿透场景只做了布隆过滤器,没做空值缓存,结果布隆过滤器误判的请求还是全打到数据库。正确的做法是两道防线一起上:布隆过滤器挡掉肯定不存在的,空值缓存抚平误判造成的少量穿透。
还有一个小坑容易被忽略:用opsForValue().set(key, value)不设置过期时间。这样 Redis 内存会持续增长,等到内存被打满,触发淘汰策略或 OOM,那就不是缓存问题了,是整个 Redis 服务不可用。
5.3 面试中怎么答这部分内容
如果你在准备面试,建议按照“一句话定义 → 触发场景 → 解决方案 → 项目实践”的顺序来回答。不要上来就背布隆过滤器,先把场景讲清楚,让面试官确认你理解到位。
我面试时会重点考察候选人是否理解“穿透、击穿、雪崩三者的本质区别”。比如问到击穿,我一定会追问:为什么互斥锁里要做二次检查?为什么锁要用 UUID 来标识?这些细节比背概念更能体现真实经验。
回答时如果能把“缓存空值 TTL 不能太长”“布隆过滤器有误判率”“锁过期时间要大于回源耗时”这类细节带出来,面试官基本会认定你是有实战经验的人。
最后分享一个我个人的习惯。每次上线前,我会把项目里的缓存 Key 过期时间都扫一遍:是否加了随机值、是否存在热点 Key 定时失效、缓存重建是否加锁。这三个问题只要都检查到位,就能绕开 90% 的缓存事故。缓存设计本身不复杂,但细节决定了线上能不能安稳运行。希望这篇总结能帮你少踩几个坑。