缓存雪崩: 是指在同一时段大量的缓存key同时失效(TTL同时到期) 或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:
1. 给不同的Key的TTL添加随机值**(因为在进行批量导入的时候TTL可能都一样)**
2. 利用Redis集群提高服务的可用性
3. 给缓存业务添加降级限流策略**(限流保护服务器)**
**4.**给业务添加多级缓存
缓存击穿: 一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
eg:多个线程同时访问一个商品页面,由于查询数据库速度较慢,多个线程同时访问就会导致多线程同时查询数据库,很多重复操作,增加数据库压力。

解决方案:
1.互斥锁
第一个线程在查询到未命中是先获取互斥锁,在开始查询数据库。
优点: 保证数据的一致性
**缺点:**会导致多个线程进行等待,降低性能

案例:

java
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 根据id查询商铺信息
*/
@Override
public Result queryById(Long id) {
//缓存穿透
//Shop shop = queryWithPassThrough(id);
//互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
if(shop==null){
return Result.fail("商铺不存在");
}
return Result.ok(shop);
}
/**
* 缓存穿透
* @param id
* @return
*/
public Shop queryWithMutex(Long id){
//从redis查缓存
String key = CACHE_SHOP_KEY+ id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
//判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//命中
if(shopJson!=null){
return null;
}
//未命中
//获取互斥锁
String lockKey = "lock:shop:"+id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
//判断是否获取成功
if(!isLock){
//失败,则休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
//根据id查询数据库
shop = getById(id);
//模拟重建延时
Thread.sleep(200);
if(shop == null){
//不存在,将控制写入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//存在,写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//释放互斥锁
unlock(lockKey);
}
//返回商铺信息
return shop;
}
2.逻辑过期
就是过期后手动处理过期,而不是立刻删除。在第一个线程获取锁后,将查询数据库的任务交给另一个线程,自己返回过期数据。此时其他线程来来访问,发现获取锁失败也返回过期数据。
优点: 提高线程利用率
**缺点:**数据不一致

案例:

缓存命中后判断是否过期,如果过期尝试获取互斥锁,获取失败就返回过期信息,如果获取到锁,就开启一个独立线程进行数据重建,并返回过期信息。
java
//线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 逻辑过期,解决缓存击穿
* @param id
* @return
*/
public Shop queryWithLogicalExpire(Long id){
//从redis查缓存
String key = CACHE_SHOP_KEY+ id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
//判断是否存在
if(StrUtil.isBlank(shopJson)){
//存在,直接返回
return null;
}
//命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, 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 {
saveShop2Redis(id,20L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
unlock(lockKey);
}
});
}
//返回过期商铺信息
return shop;
}
java
/**
* 锁
* @param key
* @return
*/
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 解锁
* @param key
* @return
*/
private void unlock(String key){
stringRedisTemplate.delete(key);
}
java
public void saveShop2Redis(Long id,Long expireSeconds) throws InterruptedException {
//查询店铺数据
Shop shop = getById(id);
Thread.sleep(200);
//封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
redisData.setData(shop);
//写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
RedisData
逻辑过期本质是永不过期,所以我们不能直接存在redis中,而是定义一个类。当程序读取到数据后 ,会比较当前时间和数据中的 expireTime。如果当前时间更大,说明数据"逻辑上"已经失效了。
java
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class RedisData {
//过期时间
private LocalDateTime expireTime;
private Object data;
}