目录
一、缓存穿透
1.什么是缓存穿透
缓存穿透是指在用户发送请求的时候,在缓存中根本不存在缓存数据,从而直接访问数据库,如果在高并发的情况下,很多请求直接访问数据库,会给数据库带来很大的压力,从而导致性能降低
2.解决办法
1. 设置空对象
在第一次查询数据库的时候没有查到就直接在缓存中写入空值,下次再查询的时候就会在缓存中进行查询了,可以较为有效的解决缓存穿透的问题
// 从redis中查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 判断商品是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
// 如果为空值返回错误信息
if (shopJson != null) {
return null;
}
// 不存在,根据id查询数据库
Shop shop = getById(id);
// 不存在,返回错误
// 这里判断shop为空 就说明数据库中也没有数据,这里我们就要设置一个空对象
// 避免下次用户访问的时候再次访问数据库,这样可以直接访问缓存,虽然是空的
// 但是避免了直接访问数据库的情况
if (shop == null) {
// 写入空值 --解决缓存穿透
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 存在,写入缓存
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 返回
return shop;
2.使用布隆过滤器
布隆过滤器相当于一个算法,在请求到达redis之前再进行一次判断查找的数据是否存在
int expectedInsertions = 1000000;
double falsePositiveRate = 0.01;
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.unencodedCharsFunnel(), expectedInsertions, falsePositiveRate);
// 添加数据到布隆过滤器
bloomFilter.put("数据1");
bloomFilter.put("数据2");
// 查询数据是否存在于布隆过滤器中
System.out.println(bloomFilter.mightContain("数据1"));
System.out.println(bloomFilter.mightContain("数据2"));
二、缓存雪崩
1.什么是缓存雪崩
缓存雪崩指的是redis中缓存的所有的key都失效或者是redis服务器宕机了,而造成所有的请求直接到达数据库,这样会给数据库造成较大的压力
2.解决办法
- 给不同的key的TTL添加随机值
- 利用redis集群进行处理,redis哨兵模式可以在redis服务器宕机时合理的分配集群中的其它服务器进行处理
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存:可以不仅仅在redis中添加缓存,还可以在nginx中添加缓存,还可以在JVM中添加本地缓存,还可以在tomcat堆内缓存等等
感兴趣的可以尝试一下
三、缓存击穿
1.什么是缓存击穿
缓存击穿问题也叫做热点key问题,就是一个被高并发访问并且重建业务逻辑比较复杂的key突然失效了,高并发的访问会给数据库带来较大的压力
2.解决方法
1.采用互斥锁的方法进行处理(自定义互斥锁)
采用互斥锁的优点:
- 没有额外的内存消耗,保证一致性
- 实现简单
缺点:
- 线程需要等待,性能受到影响
- 可能有死锁的风险
// 首先需要定义互斥锁
// 自定义互斥锁
private boolean mutexLock(String key) {
// 利用redis的setnx进行互斥锁的实现
String lockKey = LOCK_SHOP_KEY + key;
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
// 最后需要释放锁---不然其他线程一直在等待,就可能出现死锁的可能
// 删除互斥锁
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
这里什么是redis的setnx:redis的setnx是命令语句,这里指的是只有存在的当key不存在时才能编辑它的value,如果存在的话再去编辑这个key的value它的值是不会发生改变的
// 从redis中查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
// 如果为空值返回错误信息
if (shopJson != null) {
return null;
}
// 4.实现缓存重建
// 4.1获取互斥锁
// 这里线程休眠是会抛异常的,所以用try-catch,而且最终结果都会释放锁(finally)
Shop shop = null;
try {
boolean isLock = mutexLock(LOCK_SHOP_KEY + id);
// 4.2判断是否获取成功
if (!isLock) {
// 4.3失败,则休眠等待
Thread.sleep(50);
return queryWithMutex(id);
}
// 4.4成功,根据id查询数据库
shop = getById(id);
// 模拟重建缓存延时
Thread.sleep(200);
// 不存在,返回错误
if (shop == null) {
// 写入空值 --解决缓存穿透
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 存在,写入缓存
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 释放互斥锁
unLock(LOCK_SHOP_KEY + id);
}
// 返回
return shop;
2.逻辑过期时间的方法进行处理(新添加一个逻辑过期的字段)
采用逻辑过期时间的处理优点:
线程无需等待,性能较好
缺点:
- 不保证一致性
- 有额外的内存消耗
- 实现复杂
// 首先创建一个redisData类
// 这里使用组合的方法来实现,当然还可以使用继承的方法来实现
// 使用组合的方法可以使耦合度没那么高。更加灵活
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;//这里的Object相当于泛型的作用,可以支持多种类型的数据
}
// 将信息加入缓存当中----缓存预热 -- 缓存重建
public void saveShopToRedis(Long id, Long expireSeconds) throws InterruptedException {
//查询店铺数据
Shop shop = getById(id);
// 模拟延迟时间
Thread.sleep(200);
//封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
// 创建线程池 --- 线程池大小为10
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
//这里创建一个线程池是为了等会缓存重建的时候能够单独开启一个线程去进行缓存的重建
// 从redis中查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 判断缓存是否存在
if (StrUtil.isBlank(shopJson)) {
// 不存在,直接返回
return null;
}
// 命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 判断是否过期
// expireTime.isAfter是判断expireTime是否在现在的时间之后
if (expireTime.isAfter(LocalDateTime.now())) {
// 未过期,直接返回信息
return shop;
}
// 已过期, 需要重建缓存
// 缓存重建
// 获取互斥锁
boolean isLock = mutexLock(CACHE_SHOP_KEY + id);
// 判断锁是否获取成功 --- 获取成功后还要dbcheck,判断redis缓存是否过期
if (isLock) {
// expireTime.isBefore是判断expireTime是否在现在的时间之前
// if (expireTime.isBefore(LocalDateTime.now())) {
// 成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
this.saveShopToRedis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unLock(CACHE_SHOP_KEY + id);
}
});
}
// }
// 返回过期的商铺信息
return shop;
总结
缓存穿透,击穿都是高并发中比较常见的问题,在处理这类问题要判断问题是什么,然后该怎么做,用什么方法做,最主要是要逻辑清晰的知道该做什么