当前位置: 首页 > news >正文

什么是缓存穿透、缓存雪崩、缓存击穿?

 什么是缓存?

缓存就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高。

怎么防止缓存穿透?

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,给数据库带来巨大压力。

常见的解决方案有两种:

缓存空对象

客户端第一次请求是,发现数据库中数据不存在,在缓存中设置该值为空串,后面的请求中若发现缓存中存储的值为空串直接返回空串,不在查询数据库。(缓存需要设置一定时间内过期,防止数据库中有数据后缓存仍然为空。或者在插入数据时,清空缓存)

  •         优点:实现简单,维护方便
  •         缺点:额外的内存消耗,可能造成短期的不一致

    /**
     * 设置空值解决缓存穿透
     */
    public Shop queryWithPassThrough(Long id){
        //先从redis查询商铺缓存,若存在,从redis中返回,否则查询数据库,存在写入redis,并返回
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        if(StringUtils.isNotBlank(shopJson)){
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //判断命中的是否为空,防止缓存穿透
        if(shopJson==null){ //若为null,说明redis中数据为空字符串,说明mysql数据库也没有数据
            return null;
        }
        //redis不存在,查询数据库
        Shop shop = getById(id);
        if(shop==null){
            //将空值写入redis
            stringRedisTemplate.opsForValue().set("cache:shop:"+id,"",30, TimeUnit.MINUTES);
            return null;
        }
        stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop));
        return shop;
    }

布隆过滤

  •      优点:内存占用较少,没有多余key
  •      缺点:实现复杂、存在误判可能

其他方案:

        •增强id的复杂度,避免被猜测id规律

        •做好数据的基础格式校验

        •加强用户权限校验

        •做好热点参数的限流

为什么会出现缓存雪崩?

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

如何解决缓存击穿?

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

常见的解决方案有两种:

互斥锁

 在高并发环境下,当有一个线程获得锁访问数据库时,其他线程等待。假如业务A需要获取缓存A和缓存B,而业务B需要获取缓存B和缓存A。此时业务A已经获取了缓存A的锁正在等待缓存B,而业务B获取了缓存B的锁等待缓存A,就出现了互相等待的情况,产生死锁。

        优点:没有额外的内存消耗,保证一致性,实现简单

        缺点:线程需要等待,性能受影响,可能有死锁风险

   /**
     * 在设置空值,已经解决缓存穿透的基础上,添加互斥锁解决缓存击穿
     */
    public Shop queryWithMutex (Long id){
        //先从redis查询商铺缓存,若存在,从redis中返回,否则查询数据库,存在写入redis,并返回
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        if(StringUtils.isNotBlank(shopJson)){
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //判断命中的是否为空,房子缓存穿透
        if(shopJson==null){ //部位null,说明redis中数据为空字符串,说明mysql数据库也没有数据
            return null;
        }
        //redis不存在,失效缓存重建
        //获取互斥锁,每个店铺创建一个锁
        String LockKey = "lock:shop:"+id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(LockKey);
            //获取锁失败,休眠重试
            if(!isLock){
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //获取到锁,查询数据库
            shop = getById(id);
            if(shop==null){
                //将空值写入redis
                stringRedisTemplate.opsForValue().set("cache:shop:"+id,"",30, TimeUnit.MINUTES);
                return null;
            }
            stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop));
        }catch (InterruptedException e){
            throw  new RuntimeException("系统异常");
        }finally {
            //释放锁
            unLock(LockKey);
        }
        return shop;
    }
    /**
     *获取锁
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     */
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

逻辑过期:

        优点:线程无需等待,性能较好

        缺点:不保证一致性,有额外内存消耗,实现复杂

//线程池 
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    /**
     * 使用逻辑过期解决缓存击穿(实际上数据不会过期,故不需要考虑缓存穿透问题),使用时需要先缓存热点数据
     */
    public Shop queryWithLogicalExpire (Long id){
        //先从redis查询商铺缓存,若存在,从redis中返回,否则查询数据库,存在写入redis,并返回
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        //缓存中不存在,直接返回null。(热点数据,通过一般需要自行初始化到redis缓存中,一般不会出现null的情况)
        if(StringUtils.isBlank(shopJson)){
            return null;
        }
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);//获取缓存数据
        LocalDateTime expireTime = redisData.getExpireTime();//获取缓存过期时间
        //判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //未过期,直接返回
            return shop;
        }
        //过期,需要缓存重建
        //获取互斥锁
        String LockKey = "lock:shop:"+id;
        if(tryLock(LockKey)){
            //获取锁成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() ->{
                //重建缓存
                try{
                    this.saveShop2Redis(id,20L);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(LockKey);
                }
            });
        }
        return shop;
    }
    public void saveShop2Redis(Long id,Long expireSeconds){
        //查询店铺数据
        Shop shop = getById(id);
        //封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusMinutes(expireSeconds));
        //写入redis
        stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(redisData));
    }
    /**
     *获取锁
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     */
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

全局ID生成器

如果使用数据库自增ID就存在一些问题:

  • id的规律性太明显
  • 受单表数据量的限制

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:

ID的组成部分:

  • 符号位:1bit,永远为0
  • 时间戳:31bit,以秒为单位,可以使用69
  • 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

Redis自增ID策略:

  • 每天一个key,方便统计订单量
  • ID构造是 时间戳 + 计数器
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

@Component
public class RedisIdWorker {
    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    // 2022年开始时间戳
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    // 序列号位数
    private static final int COUNT_BITS = 32;
    public long nextId(String KeyPrefix) {
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond-BEGIN_TIMESTAMP;

        // 2.生成序列号
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //自增长,返回自增序列号,key不存在会自动创建一个key
        long count = stringRedisTemplate.opsForValue().increment("icr:" + KeyPrefix + ":" + date);
        // 3.拼接并返回
        // 时间戳左移32位,通过|运算,拼接序列号
        return timestamp << COUNT_BITS | count;
    }
}

http://www.dtcms.com/a/111464.html

相关文章:

  • 使用VS2022远程调试Linux项目问题
  • Linux / Windows 下 Mamba / Vim / Vmamba 安装教程及安装包索引
  • 程序化广告行业(54/89):人群标签、用户标签与Look Alike原理详解
  • 鸿蒙NEXT开发随机工具类(ArkTs)
  • 【大模型基础_毛玉仁】6.5 实践与应用--RAG、Agent、LangChain
  • FPGA--HDLBits网站练习
  • 思维链、思维树、思维图与思维森林在医疗AI编程中的应用蓝图
  • ARXML文件解析-1
  • 14.流程自动化工具:n8n和家庭自动化工具:node-red
  • 解决LeetCode“使括号有效的最少添加”问题
  • Android Hilt 教程
  • 马斯克 AI 超算
  • 蓝桥云客---九宫幻方
  • ngx_ssl_init
  • 【2-7】脉码调制
  • Apache httpclient okhttp(2)
  • 【winodws】夜神模拟器虚拟机启动失败,请进行修复。关闭hyper-v
  • CSS Id 和 Class 选择器学习笔记
  • 【嵌入式-stm32电位器控制LED亮灭以及编码器控制LED亮灭】
  • 标准库文档
  • 基于时间卷积网络TCN实现电力负荷多变量时序预测(PyTorch版)
  • 如何确保MQ消息队列不丢失:Java实现与流程分析
  • ubuntu20.04升级成ubuntu22.04
  • JavaScript BOM核心对象、本地存储
  • Linux学习笔记7:关于i.MX6ULL主频与时钟配置原理详解
  • Cribl 导入文件来检查pipeline 的设定规则(eval 等)
  • NO.64十六届蓝桥杯备战|基础算法-简单贪心|货仓选址|最大子段和|纪念品分组|排座椅|矩阵消除(C++)
  • 【如何设置Element UI的Dialog弹窗允许点击背景内容】
  • Linux系统之wc命令的基本使用
  • 华为高斯(GaussDB) 集中式数据库 的开发技术手册,涵盖核心功能、开发流程、优化技巧及常见问题解决方案