引言:
在使用 redis 的时候,不可避免的会遭遇到缓存雪崩,缓存击穿,缓存穿透的问题,在针对这写问题有多种方案,下面只是针对这些问题解决方案之一。
缓存穿透:
产生原因:
通俗的说,当一个请求访问了不存在的数据的时候,会穿过 redis 缓存直接访问数据库,如果在并发量大的情况下这种请求很有可能会导致数据库直接宕机掉,缓存穿透因此发生。
解决方案:
通用的解决方案有两种,一种是使用布隆过滤器使请求根本无法到达 redis 自然就不会有穿过redis 缓存的这种情况发生,但是因为布隆过滤器的特性(具体如下 图一 所示),也就导致了还是会存在缓存穿透的可能;第二种方案则是为对应的不存在数据设置空值这样则可以保证避免缓存穿透的发生,但是同样的一定程度上会造成的存储资源的浪费。所以这两种方案需要针对自己的业务进行合理的解决方法。这里使用的是为不存在数据设置空值的方法避免缓存穿透。
图一
代码思路流程图与具体代码如下:
代码流程图:
具体代码与代码图:
// 缓存穿透解决方案(缓存数据不存在直接访问数据库问题解决) public Shop cachePiercingSolution (Long id){ String cacheValue = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id); // 判断缓存是否命中 // 命中并存在数据 if (StrUtil.isNotBlank(cacheValue)){ return JSONUtil.toBean(cacheValue,Shop.class); } // 命中但是没有数据 if(cacheValue != null){ return null; } // 根据id查询数据库数据 Shop byId = getById(id); // 数据不存在 if (byId == null) { // 防止缓存穿透为对应 id 设置空值 stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id ,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES); return null; } // 存在返回数据并保存到缓存中 stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(byId),RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES); return byId; }
缓存击穿:
产生原因:
缓存击穿的发生通常是少量热点 key 过期导致了大量请求访问数据直接打到了数据库上造成数据库宕机。
解决方案:
解决缓存击穿这里有两种方案,第一种方案使用互斥锁解决,这种方案的缺陷是,用户在缓存未被重建的情况下,会一直等待看不到数据,造成用户体验不好,优点是用户得到的一定是最新的数据;第二种方案则是使用逻辑删除的方法进行解决(其实也就是 key 永远存在不过过期不显示),这种方案的缺陷是用户如果没有在最新的缓存建立的情况下,得到的会一直是之前的数据,不过这种方案的优点是,用户会一直得到数据的相应,不会长时间的等待数据。
利用互斥锁解决缓存击穿:
代码思路流程图与具体代码如下:
代码流程图:
具体代码与代码图:
public Shop queryWithMutex(Long id ){ // 根据 id 获取key String keyValue = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id); // 判断命中后是否存在数据 if(StrUtil.isNotBlank(keyValue)){ return JSONUtil.toBean(keyValue,Shop.class); } // 命中后数据不存在 返回数据不存在信息 if(keyValue != null){ return null; } Shop byId = null; // 缓存未命中,需要对数据库中存在数据进行缓存重建,加锁保证并发的正常使用 得到锁访问数据库,未得到锁,重试 String lockKey = RedisConstants.LOCK_SHOP_KEY + id; try { // 获取失败 if(!tryLock(lockKey)){ // 获取失败,休眠重试 Thread.sleep(50); return queryWithMutex(id); } //Thread.sleep(200); // 获取成功 byId = getById(id); // 缓存未命中数据库中不存在数据 if(byId == null){ stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES); return null; } stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id ,JSONUtil.toJsonStr(byId),RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { delLock(lockKey); } return byId; }
利用逻辑删除解决缓存击穿:
代码思路流程图与具体代码如下:
代码流程图:
具体代码与代码图:
// 2.利用逻辑删除解决缓存击穿 public Shop queryWithLogicalDel(Long id){ // 缓存中查询数据 String cacheValue = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id); // 未命中 redis 缓存直接返回空值 if (StrUtil.isBlank(cacheValue)){ return null; } // 命中 redis 缓存 判断数据是否过期 RedisData redisData = JSONUtil.toBean(cacheValue, RedisData.class); // 判断当前线程时间戳是否在预热数据的时间戳之内 if(redisData.getExpireTime().isAfter(LocalDateTime.now())){ // 在当前时间内,返回数据 return (Shop) redisData.getData(); } // 不在当前时间内,进行缓存重建 // 1.获取互斥锁,获得到,进行缓存重建,未获得返回之前数据 if(tryLock(RedisConstants.LOCK_SHOP_KEY + id)){ // 重建缓存 CACHE_REBUILD_EXECUTOR.submit(()-> { try { this.saveShopById(id, 200L); } catch (Exception e) { throw new RuntimeException(e); } finally { // 释放锁 delLock(RedisConstants.LOCK_SHOP_KEY + id); } } ); } // 未获得到锁返回之前数据 return (Shop) redisData.getData(); }
缓存雪崩:
产生原因:
当 redis 中大量 key 过期的时候造成了大量的数据打到了数据库上造成数据库宕机。
解决方案:
这里就没有代码了,只有一些概念的东西,如下图所示
总结:
在解决这些问题的时候。没有一个绝对的完美的方案,具体的方案选择需要根据业务场景去选择,当然本篇的解决方案也有很多的不足,如果有大佬斧正,欢迎私信,谢谢。