文章目录
- 一、缓存穿透
- 1、什么是缓存穿透
- 2、解决方案
- 二、缓存雪崩
- 三、缓存击穿
- 1、什么是缓存击穿
- 2、解决方案
- 3、互斥锁解决缓存击穿问题
- 4、逻辑删除解决缓存击穿问题
一、缓存穿透
1、什么是缓存穿透
- 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远都不会生效,这些请求都会打到数据库。
- 当有人恶意频繁地访问在缓存中和数据库中都不存在的数据时,整个系统就可能出现问题。
2、解决方案
- 方案一:缓存空对象。即我们把在缓存中和数据库中都不存在的数据缓存到Redis中,并设置过期时间;
优点 | 实现简单,维护方便 |
缺点 | 额外的内存消耗、也可能会造成短期的数据不一致 |
- 方案二:布隆过滤。客户端请求的数据会先请求布隆过滤器,如果存在则放行;如果不存在则拒绝客户端的请求。
优点 | 内存占用较少,没有多余key |
缺点 | 实现复杂、存在误判的可能 |
上个图就明白了:
代码实现:
- 首先查看是否命中缓存,如果没有命中,则查看数据库判断是否存在商铺,存在则写入缓存,不存在则将空值(
""
)写入redis;如果命中缓存,则查看是否是空值,如果是空值则返回给用户店铺不存在的提示,如果不是空值则返回数据给客户端。
@Override
public Result queryShopById(Long id) {
String key = CACHE_SHOP_KEY + id;
String shopCache = redisTemplate.opsForValue().get(key);
//如果在缓存中查询到商户,则返回数据给前端
if (StrUtil.isNotBlank(shopCache)) {
Shop shop = JSONUtil.toBean(shopCache, Shop.class);
return Result.ok(shop);
}
//判断命中的是否是空值
if (shopCache != null){
return Result.fail("店铺不存在");
}
//不存在则根据id在数据库中查找
Shop shop = shopMapper.selectById(id);
if (shop == null) {
//将空值写入redis,设置过期时间为2分钟
redisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
//店铺存在,写入缓存,过期时间设置为30分钟
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
流程分析图:
二、缓存雪崩
- 缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力!
解决方案:
- 给不同的key的过期时间设置随机值
- 利用redis集群提高服务的可用性
三、缓存击穿
1、什么是缓存击穿
- 缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
2、解决方案
常见的解决方案有两种:互斥锁和逻辑过期。
-
互斥锁:当线程1查询缓存未命中时,会获取一个互斥锁,然后查询数据库并重建缓存数据;在此期间,如果线程2查询缓存也未命中,并不会成功获取互斥锁(因为线程1正在使用),线程2会休眠并重试,直到线程1写入缓存释放锁;线程2缓存命中。
-
逻辑过期:当线程1查询缓存发现逻辑时间已过期时,会成功获取一个互斥锁,并开启一个新线程,这个新线程会进行查询数据库重建缓存数据的操作,写入缓存时重置逻辑过期时间,最后释放锁;线程1此时返回过期数据。如果有线程3在线程1获取互斥锁后查询缓存数据,发现逻辑时间已过期,就会获取互斥锁但失败,最后也返回过期数据,可以说是不争不抢。
解决方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | 没有额外的内存消耗、保证一致性、实现比较简单 | 线程需要等待,性能会受到影响、可能有死锁风险 |
逻辑过期 | 线程无需等待,性能较好 | 不保证一致性、有额外的内存消耗、实现复杂 |
上图:
3、互斥锁解决缓存击穿问题
@Override
public Result queryShopById(Long id) {
Shop shop = queryWithMutex(id);
if (shop == null) {
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
//封装缓存击穿方法
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
String shopCache = redisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopCache)) {
Shop shop = JSONUtil.toBean(shopCache, Shop.class);
return shop;
}
if (shopCache != null) {
return null;
}
//实现缓存重键
//获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
if (!isLock) {
Thread.sleep(50);
//没有拿到锁则重试
return queryWithMutex(id);
}
//成功
shop = shopMapper.selectById(id);
if (shop == null) {
redisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//释放互斥锁
unlock(lockKey);
}
return shop;
}
//获取锁,并设置过期时间为5秒
private boolean tryLock(String key) {
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unlock(String key) {
redisTemplate.delete(key);
}
流程分析图:
4、逻辑删除解决缓存击穿问题
@Override
public Result queryShopById(Long id) {
Shop shop = queryWithLogicalExpire(id);
if (shop == null) {
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
public Shop queryWithLogicalExpire(Long id) {
String key = CACHE_SHOP_KEY + id;
String shopCache = redisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(shopCache)) {
return null;
}
//命中,需要先把JSON反序列化为对象
RedisData redisData = JSONUtil.toBean(shopCache, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())) {
//未过期则直接返回店铺信息
return shop;
}
//已过期,需要缓存重建
//获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock) {
//成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重建缓存
this.saveShopToRedis(id, CACHE_REBUILD_TTL);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unlock(lockKey);
}
});
}
return shop;
}
/**
* @param id 店铺id
* @param expireSeconds 设置的过期时间
*/
public void saveShopToRedis(Long id, Long expireSeconds) {
//查询店铺数据
Shop shop = shopMapper.selectById(id);
//封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//写入redis
redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
//获取锁,并设置过期时间为5秒
private boolean tryLock(String key) {
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unlock(String key) {
redisTemplate.delete(key);
}
//设置10个线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
流程分析图: