什么是缓存击穿、怎么解决

文章介绍了缓存击穿问题,即高并发下热点Key失效导致大量请求直接到数据库,提出两种解决方案:使用互斥锁,在缓存失效时只有第一个线程查询数据库并更新缓存;逻辑过期,数据永不过期,过期时启动新线程更新缓存,主线程返回旧数据。同时提供了Java代码示例来展示这两种方法的实现。
摘要由CSDN通过智能技术生成

什么是缓存击穿、怎么解决

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

因为将数据存入缓存的时候,需要先执行一系列的查询操作,如果该数据访问量非常大,那也会给数据库造成很大压力。

当然不是所有的数据都要做缓存击穿处理,热点数据才需要,所以一些地方的逻辑处理会不大一样。

解决缓存击穿有两种方式:

  • 互斥锁
  • 逻辑过期

两种分别是什么意思呢?

互斥锁

缓存过期后,第一个查询的线程,先获取互斥锁再去查询数据库,在这期间如果有别的线程来查询,会获取互斥锁失败。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5dB4E792-1689435199485)(images/image-20230711213733711.png)]

逻辑过期

实质上是永不过期,只会被更新。存入缓存的数据会有一个固定的变量为过期时间,查询缓存后将变量时间与当前时间做比较,如果发现已经过期,则获取互斥锁,然后开启一个新的额外线程去查询并更新缓存,主线程先直接返回就得缓存数据。在这期间如果有其他线程访问,获取锁失败,也直接先返回旧的缓存数据。

逻辑过期的好处就是不留恋不执着,拿不到新数据就先返回旧数据。

在这里插入图片描述

下面是两种方式的实例:

互斥锁

请求进来先要获取锁

互斥锁采用向redis中存入缓存,有个方法是如果key已经存在就无法存入,满足互斥锁的特性。要区分这里的锁的key是个单独的key。不是存入实际数据的key,不要混淆。

获取锁和解锁的方法如下:

/**
 * 获取锁
 */
public boolean tryLock(String key){
    Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtils.isTrue(flag);
}
/**
 * 释放锁
 */
public void unlock(String key){
    redisTemplate.delete(key);
}

如果获取锁成功,则查询数据库,将数据存入缓存,最后一定要解锁。

如果获取锁失败,则等待一会继续调用该方法,一般第二次进入时候,就会在缓存中拿到数据直接返回

/**
 * 互斥锁解决缓存击穿
 */
private AjaxResult queryCompanyByIdByHcLock(Long id){
    String ids = Constant.COMPANY_NAME + id;
    Object o = redisTemplate.opsForValue().get(ids);
    if (o != null) {
        //判断是不是之前存入的空对象
        if (o.equals("")){
            return AjaxResult.error("公司不存在");
        }
        CompanyPo company = (CompanyPo) o;
        return AjaxResult.success(company);
    }
    /**
     * 解决缓存击穿
     */
    //获取互斥锁
    String key = "lock:company:" + id.toString();
    try {
        boolean isLock = tryLock(key);
        //判断是否获取成功
        if (!isLock){
            //失败 --> 等待一阵继续尝试流程
            Thread.sleep(50);
            return queryCompanyById(id);
        }
        //成功 --> 查库,增加缓存
        CompanyPo data = companyMapper.queryCompanyById(id);
        //如果是空的,将""存入缓存
        if (data == null) {
            redisTemplate.opsForValue().set(ids, "",5, TimeUnit.SECONDS);
            return AjaxResult.error("公司不存在!");
        }
        redisTemplate.opsForValue().set(ids, data,60, TimeUnit.SECONDS);
        return AjaxResult.success(data);
    }catch (Exception e){
        throw new RuntimeException(e);
    }finally {
        //释放锁
        unlock(key);
    }
}

逻辑过期

因为击穿显现是存在于一个热点数据,这些热点数据会事先存入redis中。

逻辑过期要给数据加上一个过期时间的字段:expireTime,所以最好重新封装一下数据。

public class RedisData {
    private LocalDateTime expireTime;
	//实际存放数据
    private Object data;

    public LocalDateTime getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(LocalDateTime expireTime) {
        this.expireTime = expireTime;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

从缓存中获取数据,如果未过期则直接返回,如果过期先获取锁,如果获取锁成功创建一个新线程去将查库并更新缓存里的数据,如果获取锁失败,则不留恋直接返回旧数据。代码如下:

private static final ExecutorService CACHE_EXECUTOR = Executors.newFixedThreadPool(10);

/**
 * 逻辑解决
 * 因为增加一个字段存过期时间,不破坏原来结构的基础上,最好是新创建一个缓存变量
 * @param id
 * @return
 */
private AjaxResult queryCompanyByIdLogic(Long id) {
    String ids = Constant.COMPANY_NAME + id;
    //【1 查询缓存】
    RedisData redisData = (RedisData) redisTemplate.opsForValue().get(ids);
    if (redisData == null) {
        return AjaxResult.error("不存在该热点数据");
    }
    CompanyPo companyPo = (CompanyPo) redisData.getData();
    LocalDateTime expireTime = redisData.getExpireTime();
    //【2 看是否过期】
    if (expireTime.isAfter(LocalDateTime.now())){
        //如果未过期,直接返回
        return AjaxResult.success(companyPo);
    }
    //如果过期
    //【3 现获取锁】
    String key = Constant.LOCAK_COMPANY_KEY + id.toString();
    boolean isLock = tryLock(key);
    if (isLock) {
        //【如果获取成功,查库,重新塞入缓存】
        try {
            CACHE_EXECUTOR.submit(() -> {
                saveData2Redis(id,30L);
            });
        }catch (Exception e){
            throw new RuntimeException(e);
        }finally {
            //【4 解锁】
            unlock(key);
        }
    }
    return AjaxResult.success(companyPo);
}

/**
 * 新增热点数据
 */
public void saveData2Redis(Long id,Long expireSeconds){
    CompanyPo companyPo = queryById(id);
    //封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(companyPo);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    //写入redis
    String key = Constant.COMPANY_NAME + id.toString();
    redisTemplate.opsForValue().set(key,redisData);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猪大侠0.0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值