跳到主要内容

如何完美解决缓存穿透

plus 版本专属

此章节是黑马点评 Plus 版本中专有的内容,而在整套文档中将普通版本和 Plus 版本都融合在了一起,让大家更方便的学习。

在如今微服务遍地的互联网项目来说,使用缓存,常见的比如 Redis,是最常用的提高效率的手段,尤其是在读多,写少的情况下更为适用。能很大程度的降低数据库的压力,但引入缓存同样也造成了一系列的问题需要解决,常见的比如缓存雪崩、缓存穿透。本文要介绍的就是关于缓存穿透的解决方案

缓存穿透

定义

缓存穿透是指查询的数据在缓存和数据库中都不存在,导致每次查询这条数据都会穿透过缓存,直接去查询数据库,相当于没有缓存一样。

会发生缓存穿透的查询代码:

public String getVoucher(String vocherId) {
String voucherRedisData = redisCache.get(vocherId);
if (StrUtil.isNotBlank(cacheData)){
return voucherRedisData;
}
String voucherData = voucherMapper.selectId(vocherId);
if (StrUtil.isNotBlank(voucherData)) {
cahce.set(vocherId, voucherData);
return voucherData;
} else {
throw new RuntimeException();
}
}

危害

一般存在缓存是为了缓解数据库的压力,如果短时间内发生了大量的请求并缓存穿透,就会试数据库的压力猛增,数据库的抗压能力比Redis要差的多得多,完全不是一个级别,所以如果是高并发的缓存穿透,极有可能造成系统宕机。

原因

  • 业务代码或者数据出现了问题
  • 恶意攻击、爬虫等造成大量缓存穿透请求

项目中优惠券的缓存穿透问题

而这种问题在查询优惠券时同样会存在,比如说某个黑客调用查看优惠券接口时,就会传入一个不存在的优惠券id,先查一遍缓存,缓存不存在则再去查询数据库,结果数据库也不存在,当并发高时,就会对数据库造成很大的压力。

解决方案

缓存空值

当查询的数据在缓存中和数据库中都不存在时,就缓存一个空结果,比如null,并将这个空结果返回给前端,并设置一个过期时间,避免消耗太多的内存

使用缓存空值的查询代码:

public String getVoucher(String vocherId) {
String voucherRedisData = redisCache.get(vocherId);
if (StrUtil.isNotBlank(cacheData)){
return voucherRedisData;
}
// 判断这个优惠券是否包含空值的缓存
Boolean voucherRedisNull = cache.hasKey("voucher_null_" + vocherId);
if (voucherRedisNull) {
throw new RuntimeException();
}
String voucherData = voucherMapper.selectId(vocherId);
if (StrUtil.isNotBlank(voucherData)) {
cahce.set(vocherId, voucherData);
return voucherData;
} else {
// 如果数据库中不存在,设置空值
Integer expireTime = 60;
redisCache.set("voucher_null_" + vocherId, expireTime);
throw new RuntimeException();
}
}

拿查看优惠券逻辑来说

  • 用户1查询优惠券id是10000,查询缓存和数据库都不存在,接着在缓存中设置一个空值,过期时间60s
  • 用户2查询优惠券id是20000,查询缓存和数据库都不存在,接着在缓存中设置一个空值,过期时间60s

问题

当短时间内有大量恶意攻击,每个请求都是的优惠券id都不同,缓存空值没有得到复用,所以还是穿透了缓存,请求都落到了数据库上,所以这种方案适合缓存空值能复用的场景。

分布式锁

可以说使用分布式锁是防止并发问题最常用的解决方案了,核心就是加一把锁,每次只有一个请求能获得到锁,没有获得锁的请求等待获得锁的请求执行完后释放锁,然后再次竞争。所以解决缓存穿透也是可以的。

拿查看优惠券逻辑来说

用户1查询优惠券请求获得锁,执行查询逻辑,用户2查询优惠券的请求需要等待锁的释放,当用户1的请求释放完锁后,用户2再执行

使用分布式锁的查询代码: