缓存穿透
就是一个根本不存在的数据 请求过来,然后 发现缓存没有,就打到数据库,然后 数据库也没有,就会给数据库造成很大的压力 ,
解决方案 就是老生常谈的 返回null值,或者布隆过滤器
我们说 返回null值 也就是 在查到数据库之后,发现数据库没有,就缓存一个null值到缓存,然后 返回回去,这样下一个请求过来了,就会读到我们redis中缓存的null值
缓存击穿
一个热点 key 突然过期了,这时候有大量的请求突然访问过来,但是缓存过期了,请求直接打到数据库,造成数据库压力过大
一般解决方案 有两种
1. 用redis的互斥锁 ,保证缓存过期的时候,只有一个线程能访问数据库,然后 构建缓存,其他线程 用自旋锁一直挂起, 在挂起的时候,线程休眠一下,然后一直获取缓存
2. 逻辑过期 原理很简单 既然缓存会过期,那你设置永不过期就行了,然后给类中添加 逻辑过期 时间,每次获得线程的时候 只要判断逻辑过期没有,逻辑过期了,就获取互斥锁,重建缓存
这样说 可能比较抽象 ,我们下面直接进行代码实现 ,保证解决缓存穿透的同时,再用互斥锁解决缓存击穿的问题
业务逻辑图
如果不理解,那就自己脑补一下多线程的情况
下面是代码实现
获取锁的类
获得锁
然后代码实现
根据id访问缓存
.
最后的逻辑实现
//互斥锁解决缓存击穿
private Result nXPassThrough(Long id) throws InterruptedException {
Result shopMap = findShopMap(id);
if (shopMap.getErrorMsg() == null) {
return Result.ok(shopMap);
}
//解决缓存击穿
//走到这里 代表着 缓存查不到了
//1. 获取 redisson 互斥锁
//2. 获取不成功 自旋等待
while (true) {
if (passThroughLock.tryLock()) {
try {
log.info("获得到了锁");
//2. 获取成功查询数据库 返回缓存数据
//没有的话查数据库
Shop shopById = query().eq("id", id).one();
if (BeanUtil.isNotEmpty(shopById)) {
//数据库有的话 添加缓存并且返回
Result shopMap1 = findShopMap(id);
if (shopMap1.getErrorMsg() == null) {
return Result.ok(shopMap1);
}
Map<String, Object> stringObjectMap = BeanUtil.beanToMap(shopById);
redisTemplate.opsForHash().putAll(RedisConstants.CACHE_SHOP_KEY + id, stringObjectMap);
// 设置过期时间 防止内存 占满
redisTemplate.expire(RedisConstants.CACHE_SHOP_KEY + id, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shopById);
}
//数据库没有 返回false
//解决缓存穿透 访问不存在的数据 缓存为null值 并且设置过期时间
log.info("缓存穿透 构建了新数据");
redisTemplate.opsForHash().put(RedisConstants.CACHE_SHOP_KEY + id, "nullId", "null");
redisTemplate.expire(RedisConstants.CACHE_SHOP_KEY + id, RedisConstants.CACHE_NULL_TTL, TimeUnit.SECONDS);
return Result.ok("返回成功");
} finally {
passThroughLock.unlock();
}
} else {
try {
Thread.sleep(100);
Result shopMap1 = findShopMap(id);
if (shopMap1.getErrorMsg() == null) {
return Result.ok(shopMap1);
}
} catch (InterruptedException e) {
return Result.fail(e.getMessage());
}
}
}
}
jmter压测结果
当我们访问数据库存在的数据
当我们访问数据库不存在的数据
也是同样的情况只查询到了一次sql,并且构建了新的空数据