新闻详情

新闻详情

首页 / 资讯中心 / 详情

一文讲透缓存穿透、击穿和雪崩

发布时间:2026/9/27 5:44:34来源:尧图网络
一文讲透缓存穿透、击穿和雪崩
面试官问穿透、击穿和雪崩有什么区别我回答先看请求的数据是否存在再看失效的是一个热点 key、很多 key还是整个缓存服务。问题请求对象典型现象主要处理缓存穿透数据库中不存在的数据未设置保护时每次请求都可能查询数据库空值缓存、布隆过滤器、参数校验缓存击穿一个访问量很高的热点 key单个 key 失效后大量请求同时回源互斥锁、逻辑过期缓存雪崩大量 key或整个 Redis 服务大量缓存同时失效或缓存故障导致集中回源随机 TTL、高可用、限流降级、多级缓存三者也可能叠加一批热门店铺同时过期形成雪崩其中某个店铺又因并发重建表现出击穿持续请求不存在的 ID 则会形成穿透流量。回答时先指出压力从哪里来再说明对应方案控制哪一段请求。面试官问什么是缓存穿透你怎么解决我回答缓存穿透是请求的数据在 Redis 和数据库中都不存在。普通缓存旁路流程只会回填查到的记录数据库没有记录时Redis 中仍没有这个 key相同的请求下次又会查询数据库。解决方案我会先校验明显非法的参数再把数据库的“无记录”结果用短 TTL 缓存起来。相同无效 ID 的后续请求命中空值标记直接结束。若无效 ID 分布很散可以在查询前加布隆过滤器提前筛掉一部分确定未加入的 ID并做好新增数据的同步维护。空值缓存只能减少同一个无效 ID 的重复查询。攻击者每次换一个新 ID 时每个 ID 仍可能造成一次数据库回源所以入口限流仍然有价值。代码实现用空字符串缓存无记录结果查询一个不存在的店铺时Redis 不能只留下“查不到”这件事。当前实现把空字符串写回 Redis并给它设置 2 分钟 TTL正常店铺数据的 TTL 是 30 分钟。这样下一次请求还能区分三种状态nullkey 不存在说明还没有缓存过需要查询数据库。空字符串key 存在但之前已经确认数据库没有记录直接返回null。非空字符串命中店铺 JSON反序列化后返回。publicShopquerywithpassthrough(Longid){StringkeyCACHE_SHOP_KEYid;StringshopJsonstringRedisTemplate.opsForValue().get(key);// 命中正常缓存if(StringUtils.isNotBlank(shopJson)){returnJSONUtil.toBean(shopJson,Shop.class);}// key 存在但值为空表示数据库确认无记录if(shopJson!null){returnnull;}// 只有 key 不存在时才回源数据库ShopshopgetById(id);if(shopnull){stringRedisTemplate.opsForValue().set(key,,CACHE_NULL_TTL,TimeUnit.MINUTES);returnnull;}stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);returnshop;}空值只表示“这一次查询没有找到记录”所以有效期要短于正常数据。店铺在这段时间内新增旧的空值仍可能挡住新数据无效 ID 很多时空值 key 留存太久也会增加 Redis 的存储压力。这套处理能挡住同一个无效 ID 的重复回源。请求每次换一个从未出现过的 ID 时Redis 没有可复用的空值仍可能触发一次数据库查询入口还需要配合参数校验和访问频率限制。追问布隆过滤器能完全替代空值缓存吗我回答:布隆过滤器只能判断“确定未加入”或“可能存在”。只要对应位置有一个为 0就可以拦截所有位置为 1 时仍需继续查询因为可能发生误判。它放行后查不到数据库的 ID还可以交给空值缓存处理重复请求。布隆过滤器需要提前装载并持续同步。新增店铺没有及时加入时业务层可能把真实存在的记录误拦截。面试官问一个热点 key 过期大量请求同时查数据库怎么办我回答这属于缓存击穿。数据在数据库中存在但一个高访问量 key 失效后多个请求同时发现未命中可能重复查询同一条记录。解决方案如果请求需要尽量新的数据我会用互斥锁让一个请求负责重建其他请求等待后重试。如果业务允许短时间旧数据我会预热逻辑过期缓存过期后先返回旧值只让一个后台任务刷新。两种方案的取舍点是等待时间与数据时效。代码实现一互斥锁控制重建缓存未命中后尝试抢锁。拿到锁的请求二次检查缓存再回源和回填没拿到锁的请求等待后重试。锁要有过期时间并且只能由持有者释放。publicShopquerywithmutex(Longid){StringkeyCACHE_SHOP_KEYid;StringshopJsonstringRedisTemplate.opsForValue().get(key);if(StringUtils.isNotBlank(shopJson)){returnJSONUtil.toBean(shopJson,Shop.class);}if(shopJson!null){returnnull;}try{for(intattempt0;attempt10;attempt){StringlockKeyLOCK_SHOP_KEYid;StringlockTokenUUID.randomUUID().toString();booleanlockedfalse;try{lockedtrylock(lockKey,lockToken);if(!locked){Thread.sleep(50);continue;}// 拿锁后再次读取避免重复回源shopJsonstringRedisTemplate.opsForValue().get(key);if(StringUtils.isNotBlank(shopJson)){returnJSONUtil.toBean(shopJson,Shop.class);}if(shopJson!null){returnnull;}ShopshopgetById(id);if(shopnull){stringRedisTemplate.opsForValue().set(key,,CACHE_NULL_TTL,TimeUnit.MINUTES);returnnull;}stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);returnshop;}finally{if(locked){unlock(lockKey,lockToken);}}}thrownewIllegalStateException(缓存重建等待超时);}catch(InterruptedExceptione){Thread.currentThread().interrupt();thrownewIllegalStateException(缓存重建被中断,e);}}privatebooleantrylock(Stringkey,Stringtoken){BooleanresultstringRedisTemplate.opsForValue().setIfAbsent(key,token,LOCK_SHOP_TTL,TimeUnit.SECONDS);returnBooleanUtil.isTrue(result);}privatestaticfinalDefaultRedisScriptLongUNLOCK_SCRIPTnewDefaultRedisScript(if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end,Long.class);privatevoidunlock(Stringkey,Stringtoken){stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(key),token);}拿到锁时请求会用UUID生成自己的锁值。释放锁前Lua 脚本先核对 Redis 中的锁值确认锁仍属于当前请求才执行删除。即使旧锁在处理期间到期、又被其他请求拿到先前的请求也删不掉新锁。DefaultRedisScript保存这段脚本RedisTemplate.execute将锁 key 和锁值传给 Redis 执行Collections.singletonList用来组装 key 列表。没有拿到锁的请求每隔 50 毫秒重试一次最多尝试 10 次仍未成功就抛出等待超时异常。锁的有效期要覆盖正常的数据库查询与缓存回填等待次数也要结合接口的超时时间调整。需要新数据、又能接受短暂等待时可以用互斥锁控制热点重建。它按店铺 ID 分锁正常持锁期间同一家店只由一个请求负责回源。如果大量不同店铺同时失效各自的锁仍会放行一次数据库查询还需要限制整体回源量。代码实现二逻辑过期与异步刷新逻辑过期把业务过期时间放进 valueRedis key 本身不设置物理 TTL。请求读到过期数据后先尝试抢锁抢到锁的请求提交异步重建任务当前请求仍返回旧值。DatapublicclassRedisData{privateLocalDateTimeexpireTime;privateObjectdata;}写入逻辑过期缓存时项目使用set保存 JSON不传入 Redis TTLpublicvoidsetwithLogicalExpire(Stringkey,Objectvalue,Longtime,TimeUnitunit){RedisDatadatanewRedisData();data.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));data.setData(value);// Redis key 不设置物理 TTLstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(data));}读取逻辑过期缓存时项目的CacheClient使用固定大小线程池异步刷新。以下示例沿用上方带 token 的trylock和unlock辅助方法放入CacheClient时需一并使用这两个方法privatestaticfinalExecutorServiceCACHE_REBUILD_POOLExecutors.newFixedThreadPool(10);publicR,IDRquerywithlogicalexpire(StringkeyPrefix,IDid,ClassRtype,FunctionID,RdbFallback,Longtime,TimeUnitunit){StringkeykeyPrefixid;StringjsonstringRedisTemplate.opsForValue().get(key);if(StringUtils.isBlank(json)){returnnull;}RedisDatadataJSONUtil.toBean(json,RedisData.class);RvalueJSONUtil.toBean((JSONObject)data.getData(),type);if(data.getExpireTime().isAfter(LocalDateTime.now())){returnvalue;}StringlockKeyLOCK_SHOP_KEYid;StringlockTokenUUID.randomUUID().toString();if(trylock(lockKey,lockToken)){booleansubmittedfalse;try{// 抢锁后重新读取确认其他请求尚未完成刷新StringlatestJsonstringRedisTemplate.opsForValue().get(key);RedisDatalatestStringUtils.isBlank(latestJson)?null:JSONUtil.toBean(latestJson,RedisData.class);if(latest!null!latest.getExpireTime().isAfter(LocalDateTime.now())){CACHE_REBUILD_POOL.submit(()-{try{RfreshValuedbFallback.apply(id);if(freshValue!null){setwithLogicalExpire(key,freshValue,time,unit);}}catch(Exceptione){log.error(缓存重建失败key{},key,e);}finally{unlock(lockKey,lockToken);}});submittedtrue;}}finally{// 未提交任务时锁仍由当前请求持有if(!submitted){unlock(lockKey,lockToken);}}}// 无论是否抢到锁都先返回旧值returnvalue;}逻辑过期缓存要先把热点数据写入 Redis。预热时先从数据库查出店铺再把店铺信息和过期时间一起保存。查询方法只读取已经写入的逻辑过期数据如果 key 不存在它会直接返回空结果不会在这个分支里再次查询数据库。逻辑过期时间到了后台线程会尝试刷新缓存。刷新失败时原有数据仍可能留在 Redis 中因此要给旧值设定最长可接受时限。超过这个时限后系统应进入受控回源或业务降级路径避免刷新持续失败时长期返回旧数据。物理 TTL 由 Redis 管理。时间到了Redis 可以删除对应的 key逻辑过期把过期时间放在 value 中key 仍然存在由应用读取过期时间并决定是否刷新。逻辑过期依赖 Redis 能够正常读取仍然需要配合复制、Sentinel 或 Cluster 等高可用方案。面试官问很多缓存同时失效或者 Redis 宕机你怎么保护数据库我回答这属于缓存雪崩需要先分清故障来源。大量 key 在相近时间写入并采用相同 TTL会集中到期Redis 节点或网络故障会让应用无法读取缓存。两种情况都可能造成大量回源。解决方案批量过期时对允许的缓存时长增加随机偏移必要时分批预热。Redis 故障时用复制、Sentinel 或 Cluster 降低节点故障影响并在应用侧限制数据库回源总量为超限请求准备明确的降级结果。业务允许旧数据时可以再用本地缓存提供一条独立读取路径。这些措施保护的范围不同随机 TTL 分散到期时间高可用处理节点故障回源限流保护数据库。本地缓存还要处理各实例的数据失效和允许多旧的问题。Redis 故障时的处理主从复制保存数据副本Sentinel 监控主从节点并执行自动故障转移Cluster 通过分片和副本支持扩展与故障转移。节点切换期间仍可能短暂失败应用客户端需要能够发现新主节点并恢复连接。缓存未命中或 Redis 读取异常时回源入口要限制数据库承受的总请求量。单 key 互斥锁只约束同一条数据的重建大量不同 key 同时失效时仍需业务级限流。超过限额的请求应返回符合业务语义的降级结果不能把 Redis 连接失败伪装成“店铺不存在”。多实例部署时各实例限额会叠加应按数据库总容量设置。本地缓存可以在 Redis 短暂不可用时提供独立的旧值读取路径但需要管理实例间更新、容量和最大允许旧值时间。代码实现给正常数据设置随机 TTL如果店铺缓存基础 TTL 为 30 分钟可以在允许的范围内加入随机偏移longbaseSecondsTimeUnit.MINUTES.toSeconds(CACHE_SHOP_TTL);longrandomSecondsThreadLocalRandom.current().nextLong(301);longttlSecondsbaseSecondsrandomSeconds;stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),ttlSeconds,TimeUnit.SECONDS);这个例子把 TTL 分布在 1800 到 2100 秒之间。随机范围必须服从业务的数据时效上限。业务数据最长只允许缓存 30 分钟时随机 TTL 可设置在 25 到 30 分钟之间。随机 TTL 只能分散物理过期时间。它不能防止单个热点并发重建也不能在 Redis 故障时继续读取缓存。逻辑过期也可以加入随机的业务过期时间以分散后台刷新任务但大量 key 仍然可能同时触发刷新所以还要配合互斥重建和回源限流。面试官追问这几种方案如何选择我回答先确认业务允许的数据旧值时长以及数据库能够承受的总回源量。条件优先考虑数据必须尽量新可以接受短暂等待互斥锁读多写少允许返回短时间旧值逻辑过期无效 ID 重复请求较多空值缓存无效 ID 分布很散数据规模大布隆过滤器加空值缓存大批缓存集中写入随机 TTL、分批预热Redis 节点可能故障复制、Sentinel 或 Cluster加限流降级面试官追问怎么证明这些方案生效了我回答在开发或隔离测试环境分别制造无效 ID、热点 key 失效和批量过期观察数据库查询量、Redis 状态和请求结果。穿透验证准备一条确认存在的店铺 ID 和一条确认不存在的 ID。无效 ID 第一次访问应查询数据库并写入空字符串空值 TTL 内再次访问时应直接结束且不再触发该方法的数据库查询。Redis 中可以使用GET、EXISTS和TTL区分空字符串 key 与不存在的 key。击穿验证先删除一个热点 key再用并发请求访问同一个 ID。互斥锁方案应看到一次缓存重建其余请求等待后命中逻辑过期方案应立即返回旧值并观察只有一个请求提交异步刷新任务。检查锁 TTL、锁释放和重建失败分支。雪崩验证批量写入测试 key 后查看 TTL 分布确认随机偏移确实产生不同到期时间。对比固定 TTL 与随机 TTL 下的数据库查询量、接口耗时、错误率和连接池等待情况。模拟 Redis 连接失败时确认请求不会无条件把全部流量转给数据库使用 Sentinel 或 Cluster 时还要观察故障发现、切换和客户端重连过程。RedisTTL命令返回剩余秒数返回-1表示 key 存在但没有物理过期时间返回-2表示 key 不存在。逻辑过期缓存通常会得到-1此时应读取 JSON 中的expireTime不能把-1当作业务数据永不过期。面试时先判断故障形状再说明方案的作用范围最后用代码交代请求在哪里停止、由谁回源、缓存如何重建。排查线上问题也遵循同一顺序先看 Redis 返回空值、缺失 key还是连接异常再看受影响的是一个热点、很多 key还是整个缓存服务。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

KEIL MDK寄存器级调试实战:System Viewer与内核寄存器查看指南 2026/9/27 6:37:23

KEIL MDK寄存器级调试实战:System Viewer与内核寄存器查看指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
做网站服务器空间避坑指南:告别被黑挂马的5个核心策略 2026/9/27 6:37:23

做网站服务器空间避坑指南:告别被黑挂马的5个核心策略

做网站服务器空间避坑指南:告别被黑挂马的5个核心策略 网站突然打不开,浏览器弹出红色安全警告,或者页面莫名其妙多了几个赌博广告链接,这是很多站长最噩梦的场景。网站被黑挂马不知道怎么办?别慌,这时候去百度搜“怎么删木马”往往为时已晚,因为你的…

阅读更多 →
3年建站老手揭秘沈阳世纪兴网站制作公司保姆级建站教程 2026/9/27 6:37:23

3年建站老手揭秘沈阳世纪兴网站制作公司保姆级建站教程

3年建站老手揭秘沈阳世纪兴网站制作公司保姆级建站教程 自己不会代码想做网站,是不是看着那些满屏的 <div> 和 CSS…

阅读更多 →
手机网站尺寸图解步骤:避坑省钱全攻略 2026/9/27 6:37:16

手机网站尺寸图解步骤:避坑省钱全攻略

手机网站尺寸图解步骤:避坑省钱全攻略 找建站公司最怕什么?怕被坑高价,更怕花大钱做出来的东西在手机上打开全是乱码、拉伸变形,或者加载慢得让人想砸手机。很多老板在咨询时,一上来就问价格,却忽略了最核心的“手机网站尺寸”适配问题,结果付了钱,发…

阅读更多 →
抖音运营底层逻辑与冷启动实操:从算法推荐到爆款内容方法论 2026/9/27 6:37:16

抖音运营底层逻辑与冷启动实操:从算法推荐到爆款内容方法论

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
RS485与LoRa参数调试工具设计原理与工程实践 2026/9/27 6:37:16

RS485与LoRa参数调试工具设计原理与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉