一、缓存穿透
1、什么是缓存穿透
- 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远都不会生效,这些请求都会打到数据库。
- 当有人恶意频繁地访问在缓存中和数据库中都不存在的数据时,整个系统就可能出现问题。
2、解决方案
- 方案一:缓存空对象。即我们把在缓存中和数据库中都不存在的数据缓存到Redis中,并设置过期时间;
- 方案二:布隆过滤。客户端请求的数据会先请求布隆过滤器,如果存在则放行;如果不存在则拒绝客户端的请求。
上个图就明白了:
代码实现:
- 首先查看是否命中缓存,如果没有命中,则查看数据库判断是否存在商铺,存在则写入缓存,不存在则将空值(
""
)写入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);
流程分析图: