1、什么是缓存击穿?
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
看这张图:当某个key突然失效后,大量的线程都在查询缓存,然后查询数据库,重建缓存,那么此时数据库的压力就变得巨大。
2、常见的两种解决方案
(1)互斥锁 (2)逻辑过期
2.1互斥锁
2.11思路分析
在每次发现未命中缓存之后,就先获取锁,获取成功,查询数据库,重建缓存,不成功,就等待。
假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。
2.12具体实现
例:根据id查询店铺类型
建表语句和Result类在缓存穿透里。
实现流程路:
代码实现:
这里先说以下锁的实现,在redis命令中,存在setnx(set not exist),当已经设置了一个锁
setnx lock 1,另一个线程再想创建一个lock 时是不成功的。
//获取锁,返回值,ture获取成功,false获取不成功
public boolean getLock(Long id){
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("cache_lock_"+id,"",10, TimeUnit.MINUTES);
return BooleanUtil.isTrue(lock);
}
//删除锁
public void removeLock(Long id){
stringRedisTemplate.delete( "cache_lock_"+id );
}
}
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.Redis_stu.entity.Result;
import com.Redis_stu.entity.ShopType;
import com.Redis_stu.mapper.queryMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class queryService {
@Autowired
private queryMapper mapper;
@Autowired
private StringRedisTemplate stringRedisTemplate;
public Result queryByType(Long id){
//查询缓存
String strJson = stringRedisTemplate.opsForValue().get("cache_shop_type_id_"+id);
//判断是否命中缓存
//***命中,返回数据
if(!StrUtil.isEmpty(strJson)){
ShopType st = JSONUtil.toBean(strJson, ShopType.class);
return Result.success(st);
}
//当value是 “” 时,说明该键值对是缓存缓存穿透后添加的
if(strJson != null){
return Result.error("无此商店类型ss");
}
//***没命中
boolean lock = getLock(id);
ShopType st = null;
try {
if(!lock){
Thread.sleep(1000);
System.out.println("稍等....");
return queryByType(id);
}
//数据库查询
st = mapper.queryById(id);
//方便多线程模拟,线程1查询数据库时慢一点
Thread.sleep(20000);
if(st==null){
//缓存穿透,添加 值为“”的数据
stringRedisTemplate.opsForValue().set( "cache_shop_type_id_"+id,"",10, TimeUnit.MINUTES);
return Result.error("无此商店类型");
}
//将数据加入缓存,有效期20分钟
stringRedisTemplate.opsForValue().set("cache_shop_type_id_"+id,JSONUtil.toJsonStr(st),20L, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
removeLock(id);
}
return Result.success(st);
}
public boolean getLock(Long id){
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("cache_lock_"+id,"",10, TimeUnit.MINUTES);
return BooleanUtil.isTrue(lock);
}
//删除锁
public void removeLock(Long id){
stringRedisTemplate.delete( "cache_lock_"+id );
}
}
2.2逻辑过期
2.21思路分析
当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。
2.22具体实现步骤
步骤一:保存过期时间expireTime
将过期时间封装在redis中存储的数据的value时,此时要么你去修改原来的实体类,换个方案,可以新建一个实体类,对原来代码没有侵入性。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
步骤二:缓存预热,通过测试文件把数据全部加入redis
public void saveRedisData(long id,long saveTime){
RedisData rd = new RedisData();
ShopType st = mapper.queryById(id);
rd.setExpireTime(LocalDateTime.now().plusSeconds(saveTime));
rd.setData(st);
stringRedisTemplate.opsForValue().set("expire_shop_type_"+id, JSONUtil.toJsonStr(rd));
}
步骤三:编写功能代码
//创建一个线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
//缓存击穿互斥锁
public ShopType queryByType(Long id){
//1查询缓存
String strJson = stringRedisTemplate.opsForValue().get("expire_shop_type_"+id);
//2判断是否命中缓存
//2.1未命中,返回数据
if(StrUtil.isEmpty(strJson)){
saveRedisData(id,20L);
return null;
}
//2.2命中,将字符串反序列化,获取expireTime和date
RedisData rd = JSONUtil.toBean(strJson, RedisData.class);
LocalDateTime ldt = rd.getExpireTime();
ShopType st = JSONUtil.toBean((JSONObject)rd.getData(), ShopType.class);
//3判断是否过期
if(ldt.isAfter(LocalDateTime.now())){
//未过期,返回数据
return st;
}
//过期
//4获取锁
String key = "cache_lock_"+id;
boolean lock = getLock(key);
//4.2获取成功开启新线程,让其查询数据,并修改缓存数据
if(!lock){
CACHE_REBUILD_EXECUTOR.submit( ()->{
try{
//重建缓存
Thread.sleep(10000);
this.saveRedisData(id,20000L);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
//释放锁
removeLock(key);
}
});
}
//4.1获取不成功,返回旧数据
return st;
}
public boolean getLock(String key){
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(key,"",10, TimeUnit.MINUTES);
return BooleanUtil.isTrue(lock);
}
//删除锁
public void removeLock(String key){
stringRedisTemplate.delete( key);
}
public void saveRedisData(long id,long saveTime){
RedisData rd = new RedisData();
ShopType st = mapper.queryById(id);
rd.setExpireTime(LocalDateTime.now().plusSeconds(saveTime));
rd.setData(st);
stringRedisTemplate.opsForValue().set("expire_shop_type_"+id, JSONUtil.toJsonStr(rd));
}