redis缓存击穿

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));
    }
  • 14
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值