欢迎来到"雪碧聊技术"CSDN博客!
在这里,您将踏入一个专注于Java开发技术的知识殿堂。无论您是Java编程的初学者,还是具有一定经验的开发者,相信我的博客都能为您提供宝贵的学习资源和实用技巧。作为您的技术向导,我将不断探索Java的深邃世界,分享最新的技术动态、实战经验以及项目心得。
让我们一同在Java的广阔天地中遨游,携手提升技术能力,共创美好未来!感谢您的关注与支持,期待在"雪碧聊技术"与您共同成长!
目录
一、缓存雪崩
1、什么是缓存雪崩?
缓存雪崩:是指在同一时段,大量的缓存key同时失效或者Redis服务宕机,导致大量请求打到数据库,带来巨大压力。
2、解决方案
二、缓存击穿
1、什么是缓存击穿?
缓存击穿:也叫热点key问题,就是一个被高并发访问、缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间打到数据库,给数据库带来巨大的压力。
举例:热点key的TTL突然到期了,线程1从redis中没命中,然后去数据库查询,并将数据重新写会redis(由于该热点key的缓存重建业务较复杂,因此重建时间比较久),在重建期间,由于该热点key是高并发的,因此此时无数线程请求该key,redis没命中,于是这些请求都打到数据库,导致数据库压力巨大。
2、解决方案
①互斥锁
核心思想:线程1发现redis不存在该热点key,于是获取互斥锁,然后查询数据库并重建该缓存,等把该热点key的缓存成功写到了redis中,才会释放这个互斥锁。这期间,其他线程,查询redis也没命中,于是也尝试获取互斥锁,但是获取失败了(因为互斥锁被线程1占用),于是无法进行查询数据库并重建缓存的动作,于是只能等待。等到线程1重建好缓存,才能命中。
优点:缓存和数据库,是数据一致的。
缺点:其他线程需要等待,导致性能不佳(服务可用性差)。
②逻辑过期
核心思想:往redis存数据时,我就不设置TTL,于是该热点key永远不会过期,因此不可能出现热点key失效的现象。
但是为了保证缓存、数据库的数据一致性,因此还是要往热点key的value当中添加一个逻辑过期时间,每次访问该热点缓存时,都要主动判断是否过期,如果过期,就获取互斥锁,然后开启一个新线程(等于叫来一个帮手),帮我去完成缓存重建的动作,我自己这个线程还是返回旧的缓存,这样的确有点数据不一致,但是也无伤大雅。
优点:无需等待,性能好(服务可用性好)。
缺点:导致轻微的数据不一致问题。
3、对比两种方案
综上:这两种方式的数据一致性、服务可用性(性能)不可兼得。
一致性和可用性的抉择,也是分布式系统里面,常常面临的一个问题。
这两种,也没有孰优孰劣,应当根据应用场景来进行抉择。
4、案例:基于"互斥锁",解决缓存击穿问题
①如何实现互斥锁呢?
我们此处借助redis的机制,来实现互斥锁。
也就是,我们获取互斥锁时,就执行setnx lock 1,即:向redis中,添加一个key为lock,value为1的键值对;释放互斥锁时,就执行del lock,即:将key为lock的数据删除。
②编写代码
先编写获取、释放互斥锁的方法。
注意:redis中的setnx命令,对应stringRedisTemplate中的setIfAbsent方法。
java
//自定义方法:获取互斥锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);//防止Boolean自动拆箱成boolean时出现空指针异常
}
//自定义方法:释放互斥锁
private void unlock(String key){
stringRedisTemplate.delete(key);
}
编写"互斥锁"解决缓存击穿的代码(带*号的,是本次要学的)
java
@Override
public Result queryById(Long id) {
String key = CACHE_SHOP_KEY + id;
//1、从Redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断redis是否存在
if(StrUtil.isNotBlank(shopJson)){
//3、存在,则直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//判断命中的是否是空值
if(shopJson != null && shopJson.equals("")){
//返回一个错误信息
return Result.fail("店铺信息不存在!防止缓存穿透!");
}
//4、redis中不存在,进行缓存重建 *
//4.1获取互斥锁 *
String lockKey = "lock:shop:"+id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
//4.2判断是否获取成功 *
if(!isLock){
//4.3失败,则休眠并重试查询redis *
Thread.sleep(50);
return queryById(id);//递归本方法,表示重新查询redis
}
//4.4成功,根据id查询数据库 *
shop = getById(id);//该方法,来自mybatisPlus
//模拟重建缓存的延时 *
Thread.sleep(200);
//5、数据库也不存在,返回错误 *
if(shop == null){
//将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在!");
}
//6、数据库中存在,则写入redis,并返回给前端 *
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//7、释放互斥锁 *
unlock(lockKey);
}
if(shop==null){ //*
return Result.fail("店铺不存在!");
}
//8、返回
return Result.ok(shop);
}
③重启项目,使用Jmeter工具测试该后端接口
可见此时没毛病,使用"互斥锁"的方式,成功解决了"缓存击穿"的问题。