mid - 缓存穿透 & 击穿 & 雪崩

缓存

缓存就是数据交换的缓冲区,
是存贮数据的临时地,一般读写性能较高
Pasted image 20260329203805

添加商户缓存

Mybatis查询用户提供 query.function()各种函数
fef64afc67f0de5fbf40398150fd8317ccdf678f

queryById

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Result queryShopById(Long id){
//1.先走Redis缓存
String key = CACHE_SHOP_KEY + id;
String val = stringRedisTemplate.opsForValue().get(key);

//2.检查key-val的存在
if(StrUtil.isNotBlank(val)){
Shop valToBean = JSONUtil.toBean(val,Shop.class);
return Result.ok(valToBean);
}

//3.不存在查数据库
Shop shop = getById(id);
if(shop==null) return Result.fail("店铺不存在");

//4. 写入Redis缓存
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJSONStr(shop));
return Result.ok(shop);
}

queryByType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//用String实现
@Override
public List<ShopType> queryByShopType(){
String key = "shop:type";
String val = stringRedisTemplate.opsForValue().get(key);

//有缓存, 直接返回
if(StrUtil.isNotBlank(val)) return JSONUTil.toList(val,ShopType.class);

//查询数据库
List<ShopType> typeList = query().orderByAsc("sort").list();
if(typeList==null) return new ArrayList<>();

stringRedisTemplate.opsForValue().set(key,JSONUtil.toJSONStr(typeList));
reture typeList;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//用list实现
List<String> range = stringRedisTemplate.opsForList()
.range(key,O, -1); //表示全部

//如果缓存中有,那么返回
if (range != null && !range.isEmpty()) {
return range.stream()
.map(s -> JsoNUtil.toBean(s,ShopType.class))
.collect( Collectors.toList() );
}

//如果缓存中没有,那么查询数据库
List<ShopType> typeList = query().orderByAsc("sort").list();
if (typeList == null) return new ArrayList<>();

//将查询结果放入缓存
typeList.forEach(ShopType t -> stringRedisTemplate.opsForList()
.rightPush(key, JSoNUtil.toJsonStr(t)));

return typeList;

实现商铺的缓存和数据库的双写一致

先更新数据库, 再删除缓存, 下次访问在缓存找, 没找到再写缓存

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public Result updateShop(Shop shop){
Long id = shop.getId();
if(id==null) return Result.fail("店铺id为空");
//先更新数据库
updateById(shop);

//再更新缓存
stringRedisTemplate.delete(CAHE_SHOP_KEY+shop.getID());
return Result.ok();
}

缓存穿透 - 不存在的data

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
不断的请求缓存和数据库中都不存在的数据, 可能导致数据库的崩溃.

flowchart LR
    C[客户端] --|1. 未命中|--> R[Redis]

    R --|2. 未命中|--> DB[数据库]
解决方案:
缓存空对象 NonKey->Null

实现简单, 但是额外内存消耗, 短期的不一致

布隆过滤 (言假必定假)

hash+二进制位 来判断是否有
Pasted image 20260329141538

其他方案
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

我们在生产中常用的是赋空值 “”

大概流程:
1.redis查询数据
2.判断是不是有数据的 isblank
3.判断是不是””,fail
4.只能是null了,走数据库
5.数据库找到为null,则redis缓存空值 “”,fail
6.非null,写入redis并返回数据

6c1cbbfbfeb0f93f5cd2ef25fb8c18a77c3b6bd3

缓存击穿

一个被高并发访问并且缓存重建业务较复杂key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

Pasted image 20260329141902
解决方案 优点 缺点
互斥锁 没有额外的内存消耗 线程需要等待,性能受影响
保证一致性 可能有死锁风险
实现简单
逻辑过期 线程无需等待,性能较好 不保证一致性
有额外内存消耗
实现复杂

互斥锁

缓存击穿的互斥锁属于悲观锁的一种应用
——为了避免并发冲突而先加锁,
只不过它是跨进程或者跨服务的分布式锁形式,
不是数据库内部的锁。
Pasted image 20260329142202

逻辑过期

设置逻辑过期时间,
发现过期则加锁异步刷新, 并且返回旧数据 –> 数据不一致
Pasted image 20260329142222

缓存雪崩

指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案

  • 不同的Key的TTL添加随机值 –> 防止同时失效
  • 利用Redis集群 –> planB
  • 给缓存业务添加降级限流策略 –> 限制访问防止DB垮
  • 给业务添加多级缓存

Pasted image 20260329142735!