一、缓存穿透
定义
查询一个数据库里面根本不存在的数据
Redis 查不到 → 去查 MySQL;MySQL也查不到。
缓存永远不会生效,每一次请求都会直接打到数据库。
举例子:
商铺 id 数据库最大只有 10,但是有人疯狂请求id=-1、id=99999。
- Redis:
cache:shop:-1不存在 → 返回null - 代码去查 MySQL,数据库没有这条记录,返回
null - 不会往Redis存任何东西
- 下一次再来请求
-1,流程又重复一遍,又访问数据库
如果恶意发起成千上万次这种不存在 id 的请求,数据库压力直接被打崩,这就是缓存穿透。
区分记忆:
- 缓存击穿:数据是存在的,只是缓存刚好过期
- 缓存穿透:这条数据压根就不存在于数据库
解决方法1:缓存空值(小项目首选)
核心思路
数据库查不到数据时,向Redis存一个空字符串"",并且设置一个短过期时间(2分钟)。
下次再来相同请求:
Redis拿到"",直接返回失败,不再访问数据库。
你项目里面对应的核心代码
@OverridepublicResultqueryById(Longid){// 1. 从 redis 查询商铺缓存Stringkey=CACHE_SHOP_KEY+id;StringshopJson=stringRedisTemplate.opsForValue().get(key);// 2. 判断是否存在,有真实的商铺JSON数据if(StrUtil.isNotBlank(shopJson)){// 3. 存在,直接返回Shopshop=JSONUtil.toBean(shopJson,Shop.class);returnResult.ok(shop);}// 判断命中的是否是空值【解决:缓存穿透】// shopJson 不是 null,但却是 "" 空字符串(命中空缓存)if(shopJson!=null){// 说明 Redis 存了空字符串,代表缓存不存在returnResult.fail("商铺不存在");}// 4. 缓存未命中,查询数据库// 只有 shopJson == null 的时候,才能跑到这里Shopshop=getById(id);// 5. 数据库没查到if(shop==null){// 写入空值,过期时间 2 分钟stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.SECONDS);returnResult.fail("商铺不存在");}// 6. 查询到数据,写入 RedisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);// 返回结果returnResult.ok(shop);}# 什么时候会进到数据库查询(shopJson==null)1.第一次访问这个id,Redis里面还没有创建任何key;2.之前存的空字符串 `""` 过期了,Redis自动把这个key删掉了;3.Redis缓存被手动清空。✅优点:简单好实现
❌缺点:Redis会存很多没用的空key,占用一点点内存
方案2:布隆过滤器(海量大数据场景)
思路
提前把数据库所有合法id全部放进布隆过滤器。
请求进来,先经过布隆过滤器校验。
- 如果过滤器判定:id不存在 → 直接拦截,Redis、数据库都不去访问
- 如果过滤器判定:id存在 → 才放行,查询缓存、数据库
✅优点:完全拦截无效请求,不会产生大量空key
❌缺点:实现复杂;布隆过滤器有极小误判概率;新增数据要维护过滤器;小型项目一般不用
方案3:接口参数校验(第一道防线)
在最开始就拦截非法参数,不往下走缓存逻辑
比如:商铺id必须大于0
if(id<=0){returnResult.fail("id非法");}三、快速区分三大缓存问题
| 问题 | 本质 | 解决方案 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据,每次请求直达数据库 | 缓存空值、布隆过滤器、参数校验 |
| 缓存击穿 | 热点key缓存过期,大量并发同时打到数据库 | 互斥锁、逻辑过期 |
| 缓存雪崩 | 大批量缓存key同时过期,数据库压力瞬间暴涨 | 过期时间加上随机偏移值 |