目录
一、缓存穿透
1.简介
2.解决方案
3.修改前的代码
4.修改过后的代码
二、缓存雪崩
1.简介
2.解决方案
三、缓存击穿
1.简介
2.解决方案
3.用代码来实现互斥锁来解决缓存击穿
4.用代码来实现逻辑过期解决缓存击穿
四、缓存穿透和缓存击穿的区别
一、缓存穿透
1.简介
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
也就是有请求一请求到后端就一定会打到数据库(redis和MySQL都没有该数据),打一次不要紧,但要是有无数个请求同时打过来,那数据库大概率会崩溃,所以预防缓存穿透问题还是十分重要的。
2.解决方案
目前常见的解决方案有返回缓存空对象和布隆过滤两种方法,区别如下:
-
缓存空对象
-
优点:实现简单,维护方便
-
缺点:
-
额外的内存消耗
-
可能造成短期的不一致
-
-
-
布隆过滤
-
优点:内存占用较少,没有多余key
-
缺点:
-
实现复杂
-
存在误判可能
-
-
缓存空对象的解决思路如下:
当一个请求直接打到数据库的时候,后端直接在Redis中返回个空对象,之后多次相同的请求都会止步于Redis,通俗易懂的话来说,要打打Redis,别打MySQL。
布隆过滤的解决思路如下:
布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,
假设布隆过滤器判断这个数据不存在,则直接返回
这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突
3.修改前的代码
我们采用返回空对象的方法来缓解缓存穿透问题,修改前的代码如下:
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 根据id查询店铺信息* @param id* @return*/@Overridepublic Result queryById(Long id) {String key = RedisConstants.CACHE_SHOP_KEY + id;//1.从redis中查询商铺缓存信息String shopJson = stringRedisTemplate.opsForValue().get(key);//2、判断是否存在if (StrUtil.isNotBlank(shopJson)) {//3、存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}//4、不存在,根据id查询数据库Shop shop = getById(id);//5、判断数据库中是否有数据if (shop == null) {//6、没有,返回错误return Result.fail("店铺不存在");}//7、有数据,返回,并将数据写入redisString jsonStr = JSONUtil.toJsonStr(shop);stringRedisTemplate.opsForValue().set(key,jsonStr);//8、返回结果return Result.ok(shop);}
}
这段代码的详细分析在以下文章可查看:给查询业务添加Redis缓存
4.修改过后的代码
@Overridepublic Result queryById(Long id) {String key = RedisConstants.CACHE_SHOP_KEY + id;//1.从redis中查询商铺缓存信息String shopJson = stringRedisTemplate.opsForValue().get(key);//2、判断是否存在if (StrUtil.isNotBlank(shopJson)) {//3、存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}//判断命中的是否为空值------------------------------------------->修改后if(shopJson != null){return Result.fail("店铺不存在");}//4、不存在,根据id查询数据库Shop shop = getById(id);//5、判断数据库中是否有数据if (shop == null) {//6、没有,返回空字符串给redis------------------------------->修改后stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);return Result.fail("店铺不存在");}//7、有数据,返回,并将数据写入redisString jsonStr = JSONUtil.toJsonStr(shop);stringRedisTemplate.opsForValue().set(key,jsonStr,30L, TimeUnit.MINUTES);//8、返回结果return Result.ok(shop);}
二、缓存雪崩
1.简介
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
2.解决方案
-
给不同的Key的TTL添加随机值
-
利用Redis集群提高服务的可用性
-
给缓存业务添加降级限流策略
-
给业务添加多级缓存
三、缓存击穿
1.简介
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
2.解决方案
常见的解决方案有两种:
-
互斥锁
-
逻辑过期
互斥锁的解决思路就是在Redis进行缓存重建时,拿到一个互斥锁,其他请求拿不到这个锁就是乖乖等待锁的释放。
逻辑过期的解决思路如下:
在存入redis的value中增加一个字段,该字段为过期时间加上x分钟,通过计算就知道这个数据是否逻辑上过期,事实上没过期一直存在redis中。
在redis进行缓存重建的时候,会另开一个线程进行重建并拿到互斥锁,其他线程拿不到数据想要缓存重建时也拿不到锁,那就直接返回旧数据。
互斥锁方案:由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响
逻辑过期方案: 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦
3.用代码来实现互斥锁来解决缓存击穿
其中这个互斥锁跟我们平时用的锁不太一样,不是synchronized等那些锁,这个互斥锁是我们自己编写的锁,其核心是利用redis中的setnx指令来实现这个互斥锁。(共享+先到先到+不会被其他线程修改)
获取锁和释放锁的代码如下:
private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);
}private void unlock(String key) {stringRedisTemplate.delete(key);
}
Service代码如下:
public Shop queryWithMutex(Long id) {String key = CACHE_SHOP_KEY + id;// 1、从redis中查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get("key");// 2、判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判断命中的值是否是空值if (shopJson != null) {//返回一个错误信息return null;}// 4.实现缓存重构//4.1 获取互斥锁String lockKey = "lock:shop:" + id;Shop shop = null;try {boolean isLock = tryLock(lockKey);// 4.2 判断否获取成功if(!isLock){//4.3 失败,则休眠重试Thread.sleep(50);return queryWithMutex(id);}//4.4 成功,根据id查询数据库shop = getById(id);// 5.不存在,返回错误if(shop == null){//将空值写入redisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);//返回错误信息return null;}//6.写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL,TimeUnit.MINUTES);}catch (Exception e){throw new RuntimeException(e);}finally {//7.释放互斥锁unlock(lockKey);}return shop;}
4.用代码来实现逻辑过期解决缓存击穿
流程图如下:
具体代码如下:
@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {String key = CACHE_SHOP_KEY + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期,直接返回店铺信息return shop;}// 5.2.已过期,需要缓存重建// 6.缓存重建// 6.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock){CACHE_REBUILD_EXECUTOR.submit( ()->{try{//重建缓存this.saveShop2Redis(id,20L);}catch (Exception e){throw new RuntimeException(e);}finally {unlock(lockKey);}});}// 6.4.返回过期的商铺信息return shop;
}
四、缓存穿透和缓存击穿的区别
缓存穿透和缓存击穿之间的区别,可以说是既有质的区别也有量的区别:
-
质的区别:
- 数据存在性不同:缓存穿透是查询一个数据库中不存在的数据,而缓存击穿是查询一个数据库中确实存在且非常热门的数据。
- 触发原因不同:缓存穿透是由于查询的数据在数据库中不存在,导致缓存中也没有对应的记录;缓存击穿则是由于缓存中的数据到期,而此时有大量请求需要访问这个数据。
-
量的区别:
- 请求量不同:缓存穿透可能涉及到的请求量相对较小,因为通常不会有很多请求同时查询一个不存在的数据;而缓存击穿则可能涉及到大量的请求,因为热门数据的访问量通常很大。
- 影响范围不同:缓存穿透的影响范围可能较小,因为它只涉及到查询不存在的数据;而缓存击穿的影响范围可能较大,因为它涉及到的是热门数据,可能会对数据库造成较大压力。
总的来说,缓存穿透和缓存击穿在触发原因、数据存在性、请求量和影响范围等方面都存在明显的区别,这些区别既包括了质的不同,也包括了量的不同。因此,在设计缓存策略时,需要针对这两种情况分别采取不同的解决方案。