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

Redis缓存之预热、击穿、穿透、雪崩

面试切入点
在这里插入图片描述

在这里插入图片描述
缓存预热
在这里插入图片描述
什么是预热?
mysql假如新增100条记录,一般默认以mysql为准作为底单数据,如何同步到redis(布隆过滤器),这100条合法数据??
为什么需要预热?
mysql有100条新记录,redis无
1.比较懒,什么都不做,只对mysql做了数据新增,利用redis的回写机制,让他逐步实现100条新增记录的同步。最好提前比如晚上部署发布版本的时候,由自己人提前做一次,让redis同步了,不要把这个问题留给客户。
2.通过中间件或者程序自行完成@PostConstruct。
缓存雪崩
发生

  • redis主机挂了,Redis全盘崩溃,偏硬件运维
  • redis中有大量key同时过期,大面积失效,偏软件开发

预防+解决

  • redis中key设置为永不过期或者过期时间错开

  • redis缓存集群实现高可用(主从+哨兵、Redis Cluster、开启Redis持久化机制aof或者rdb,尽快恢复缓存数据)

  • 多缓存结合预防雪崩(ehcache本地缓存+redis缓存)

  • 服务降级(Hystrix或者阿里sentinel限流或降级)
    在这里插入图片描述
    在这里插入图片描述

  • 人民币玩家 (阿里云-云数据库Redis版)

缓存穿透
是什么?
请求去查询一条记录,先看redis无,后查mysql无,都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库的压力暴增,这种现象我们称为缓存穿透,这个redis变成了摆设。
简单来说,本来无一物,两库都没有。既不在Redis缓存库,也不在mysql,数据库存在被多次暴击风险。

解决
在这里插入图片描述
在这里插入图片描述方案1:空对象缓存或者缺省值

  • 一般情况OK
    在这里插入图片描述

  • 黑客攻击

    黑客会对你的系统进行攻击,拿一个不存在的id去查询数据,会产生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉。
    key相同打你系统
    第一次打到mysql,空对象缓存后第二次就返回defaultNull缺省值,避免mysql被攻击,不用再到数据库中去走一圈了
    key不同打你系统
    由于存在空对象缓存和缓存回写(看自己业务不限死),redis中的无关紧要的key也会越写越多(记得设置redis过期时间)

方案2:Google布隆过滤器Guava解决缓存穿透
Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们可以直接使用Guava布隆过滤器
案例:白名单过滤器
架构
在这里插入图片描述
误判问题,但是概率小可以接受,不能从布隆过滤器删除 。全部合法的key都需要放入Guava版布隆过滤器+redis里面,不然数据返回就是null

引入pom

 <!--guava Google 开源的 Guava 中自带的布隆过滤器-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>

入门使用

 /**
     * 测试布隆过滤器 demo
     */
    @Test
    public void testGuavaBloomFilter(){
        //1.创建guava版本的布隆过滤器
        BloomFilter<Integer> integerBloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100);
        //2.判断指定的元素是否存在
        System.out.println(integerBloomFilter.mightContain(1));
        System.out.println(integerBloomFilter.mightContain(2));
        System.out.println();
        //3.往过滤器加入元素
        integerBloomFilter.put(1);
        integerBloomFilter.put(2);
        System.out.println(integerBloomFilter.mightContain(1));
        System.out.println(integerBloomFilter.mightContain(2));

    }

运行效果
在这里插入图片描述
取样本数据100W,查查不在这个样本范围内的10W数据是否存在??

Controller层

import com.atguigu.redis7.service.GuavaBloomFilterService;
import com.atguigu.redis7.service.GuavaFilterService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;


@Api(tags = "google工具Guava处理布隆过滤器")
@RestController
@Slf4j
public class GuavaBloomFilterController
{
    @Resource
    private GuavaFilterService guavaBloomFilterService;

    @ApiOperation("guava布隆过滤器插入100万样本数据并额外10W测试是否存在")
    @RequestMapping(value = "/guavafilter",method = RequestMethod.GET)
    public void guavaBloomFilter()
    {
        guavaBloomFilterService.guavaBloomFilter();
    }
}

service层

package com.atguigu.redis7.service;

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;


@Service
@Slf4j
public class GuavaFilterService
{
    //1 定义一个常量
    public static final int _1W = 10000;
    //2 定义我们guava布隆过滤器,初始容量
    public static final int SIZE = 100 * _1W;
    //3 误判率,它越小误判的个数也就越少(思考,是否可以是无限小??没有误判岂不是更好)
    public static double fpp = 0.03;//0.01 0.000000000000001
    //4 创建guava布隆过滤器
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE,fpp);


    public void guavaBloomFilter()
    {
        //加入100W白名单的数据
        for (int i = 1; i <=SIZE ; i++) {

            bloomFilter.put(i);
        }
       //取10W个不在合法范围内的数据进行布隆过滤器的误判验证
        ArrayList<Integer> list = new ArrayList<>(10 * _1W);

       //验证
        for (int i = SIZE+1; i <=SIZE+10*_1W ; i++) {
            if(bloomFilter.mightContain(i)){
                log.info("被误判了:{}",i);
                list.add(i);
            }
            
        }

        log.info("误判总数量:{}",list.size());

    }
}

实际效果
在这里插入图片描述
误判结果:3033
在这里插入图片描述
在这里插入图片描述
误判率的大小与坑位以及hash函数的关系:
0.03的误判率,hash函数是5,但是0.01时变成7,同时坑位数目变多。
在这里插入图片描述
默认误判率为0.03
在这里插入图片描述
布隆过滤器说明
在这里插入图片描述
实现黑名单机制
在这里插入图片描述
缓存击穿
是什么?
大量的请求同时查询一个key时,此时的这个key正好失效了,就会导致大量的请求都打到数据库上面去了。
简单说就是热点key突然失效了,暴打Mysql
备注:穿透和击穿,截然不同
在这里插入图片描述

危害

  • 会造成某一时刻数据库的请求量过大,压力剧增
  • 一般技术部门**需要知道热点key是那些个?**做到心里有数防止击穿
    解决
    在这里插入图片描述
    热点key失效
  • 时间到了自然清除但还被访问到
  • delete掉的key,刚巧又被访问
    解决方案
  • 方案1:差异失效时间,对于访问频繁的热点key,干脆就不设置过期时间
  • 方案2:互斥更新,采用双重校验加锁的策略
    在这里插入图片描述

案例:天猫聚划算功能实现+防止缓存击穿
在这里插入图片描述
问题:热点key突然失效了,导致缓存击穿
技术方案实现:
分析过程:
在这里插入图片描述
redis数据类型选型
在这里插入图片描述
业务类

package com.atguigu.redis7.entities;

import io.swagger.annotations.ApiModel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;


@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "聚划算活动producet信息")
public class Product
{
    //产品ID
    private Long id;
    //产品名称
    private String name;
    //产品价格
    private Integer price;
    //产品详情
    private String detail;
}

controller层

package com.atguigu.redis7.controller;

import com.atguigu.redis7.entities.Product;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;


@RestController
@Slf4j
@Api(tags = "聚划算商品列表接口")
public class JHSProductController
{
    public  static final String JHS_KEY="jhs";
    public  static final String JHS_KEY_A="jhs:a";
    public  static final String JHS_KEY_B="jhs:b";

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 分页查询:在高并发的情况下,只能走redis查询,走db的话必定会把db打垮
     * @param page
     * @param size
     * @return
     */
    @RequestMapping(value = "/pruduct/find",method = RequestMethod.GET)
    @ApiOperation("聚划算案例,每次1页每页5条显示")
    public List<Product> find(int page, int size) {
        List<Product> list=null;

        long start = (page - 1) * size;
        long end   = start + size - 1;

        try
        {
            // 采用redis list结构里面的lrang命令来实现加载和分页查询
            list = redisTemplate.opsForList().range(JHS_KEY,start,end);
            if(CollectionUtils.isEmpty(list))
            {
                //TODO 走mysql查询
            }
            log.info("参加活动的商家:{}",list);
        }catch (Exception e){
            // 出异常了,一般redis宕机了或者redis网络抖动导致timeout
            log.error("jhs exception:{}",e);
            e.printStackTrace();
            // ....再次查询mysql
        }
        return list;
    }


    @RequestMapping(value = "/pruduct/findab",method = RequestMethod.GET)
    @ApiOperation("AB双缓存架构,防止热点key突然失效")
    public List<Product> findAB(int page, int size) {
        List<Product> list=null;

        long start = (page - 1) * size;
        long end   = start + size - 1;

        try
        {
            list = redisTemplate.opsForList().range(JHS_KEY_A,start,end);
            if(CollectionUtils.isEmpty(list))
            {
                log.info("---A缓存已经过期失效或活动结束了,记得人工修改,B缓存继续顶着");
                list = redisTemplate.opsForList().range(JHS_KEY_B,start,end);
                if(CollectionUtils.isEmpty(list))
                {
                    //TODO 走mysql查询
                }
            }
        }catch (Exception e){
            // 出异常了,一般redis宕机了或者redis网络抖动导致timeout
            log.error("jhs exception:{}",e);
            e.printStackTrace();
            // ....再次查询mysql
        }

        return list;
    }

}

采用定时器将参与聚划算的特价商品写入redis中


@Service
@Slf4j
public class JHSTaskService
{
    public  static final String JHS_KEY="jhs";
    public  static final String JHS_KEY_A="jhs:a";
    public  static final String JHS_KEY_B="jhs:b";

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 偷个懒不加mybatis了,模拟从数据库读取20件特价商品,用于加载到聚划算的页面中
     * @return
     */
    private List<Product> getProductsFromMysql() {
        List<Product> list=new ArrayList<>();
        for (int i = 1; i <=20; i++) {
            Random rand = new Random();
            int id= rand.nextInt(10000);
            Product obj=new Product((long) id,"product"+i,i,"detail");
            list.add(obj);
        }
        return list;
    }

    @PostConstruct
    public void initJHS()
    {
        log.info("启动定时器天猫聚划算功能模拟开始......,O(∩_∩)O哈哈~");

        //1 用线程模拟定时任务,后台任务定时将mysql里面的参加活动的商品刷新到redis里
        new Thread(() -> {
            while (true)
            {
                //2 模拟从mysql查出数据,用于加载到redis并给聚划算页面显示
                List<Product> list = this.getProductsFromMysql();
                //3 采用redis list数据结构的lpush命令来实现存储
                redisTemplate.delete(JHS_KEY);
                //4 加入最新的数据给redis参加活动
                redisTemplate.opsForList().leftPushAll(JHS_KEY,list);
                //5 暂停1分钟线程,间隔一分钟执行一次,模拟聚划算一天执行的参加活动的品牌
                try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            }
        },"t1").start();

    }

}

目前的效果
在这里插入图片描述
redis中的数据
在这里插入图片描述
请求接口
在这里插入图片描述
响应数据
在这里插入图片描述
基本功能完成,请思考一下在高并发的情境下有什么经典问题??
Bug和隐患说明

  • 热点key突然失效导致可怕的缓存击穿
    在这里插入图片描述
    delete命令执行的一瞬间有空隙,其他请求线程继续找Redis为null,打到了mysql,暴击。
    在这里插入图片描述
    最终目的:2条命令的原子性还是其次,主要是防止热key突然失效暴击mysql,打爆系统

互斥更新与双重校验登场
在这里插入图片描述
差异失效时间登场
在这里插入图片描述
代码实现
在这里插入图片描述

两块缓存,先更新缓存B,再更新缓存A

  @PostConstruct
    public void initJHSAB(){
        log.info("启动AB定时器计划任务天猫聚划算功能模拟.........."+DateUtil.now());

        //1 用线程模拟定时任务,后台任务定时将mysql里面的参加活动的商品刷新到redis里
        new Thread(() -> {
            while (true)
            {
                //2 模拟从mysql查出数据,用于加载到redis并给聚划算页面显示
                List<Product> list = this.getProductsFromMysql();
                //3 先更新B缓存且让B缓存过期时间超过A缓存,如果A突然失效了还有B兜底,防止击穿
                redisTemplate.delete(JHS_KEY_B);
                redisTemplate.opsForList().leftPushAll(JHS_KEY_B,list);
                redisTemplate.expire(JHS_KEY_B,86410L,TimeUnit.SECONDS);
                //4 再更新A缓存
                redisTemplate.delete(JHS_KEY_A);
                redisTemplate.opsForList().leftPushAll(JHS_KEY_A,list);
                redisTemplate.expire(JHS_KEY_A,86400L,TimeUnit.SECONDS);

                //5 暂停1分钟线程,间隔一分钟执行一次,模拟聚划算一天执行的参加活动的品牌
                try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            }
        },"t1").start();
    }

查询逻辑:先查询A,再查询B,缓存都没有,才去查询数据库

 @RequestMapping(value = "/pruduct/findab",method = RequestMethod.GET)
    @ApiOperation("AB双缓存架构,防止热点key突然失效")
    public List<Product> findAB(int page, int size) {
        List<Product> list=null;

        long start = (page - 1) * size;
        long end   = start + size - 1;

        try
        {
            list = redisTemplate.opsForList().range(JHS_KEY_A,start,end);
            if(CollectionUtils.isEmpty(list))
            {
                log.info("---A缓存已经过期失效或活动结束了,记得人工修改,B缓存继续顶着");
                list = redisTemplate.opsForList().range(JHS_KEY_B,start,end);
                if(CollectionUtils.isEmpty(list))
                {
                    //TODO 走mysql查询
                }
            }
        }catch (Exception e){
            // 出异常了,一般redis宕机了或者redis网络抖动导致timeout
            log.error("jhs exception:{}",e);
            e.printStackTrace();
            // ....再次查询mysql
        }

        return list;
    }

实现效果
在这里插入图片描述
接口调用
在这里插入图片描述
在这里插入图片描述
A失效了
在这里插入图片描述
B兜底
在这里插入图片描述
在这里插入图片描述
总结
在这里插入图片描述

视频链接
Redis缓存之预热、击穿、穿透、雪崩

相关文章:

  • yolov8几种模型参数model 解读
  • 【MYSQL从入门到精通】数据类型及建表
  • 牛客 小红杀怪
  • 代码随想录算法训练营第十三天
  • FFT DFT 示波器
  • 期权时间价值与隐含波动率怎么选?
  • [特殊字符] 超轻高性能的 Rust HTTP 服务器 —— Hyperlane [特殊字符][特殊字符]
  • VSCode、clangd、mingw 配置与使用
  • 数据结构篇:线性表的另一表达—链表之单链表(上篇)
  • 地理数据输出
  • 解决缓存穿透的布隆过滤器与布谷鸟过滤器:谁更适合你的应用场景?
  • dify文本生成图片
  • 在Ubuntu系统如何让MySQL服务器支持远程连接
  • Elucidating the Design Space of Diffusion-Based Generative Models
  • 【Pandas】pandas DataFrame bool
  • 人工智能与认知科学的交汇:机器是否能“理解”?
  • 【Springboot知识】Springboot进阶-Micrometer指标监控深入解析
  • wsl中迁移ubuntu24.04后docker后无法启动的问题
  • 聊一聊接口测试时需要注意哪些
  • FPAG_BUFFER学习
  • 网站开发协议范本/谷歌关键词热度查询
  • 做视频网站视频放在哪里/可以发广告的100个网站
  • 微网站服务器/怎么把产品推广到各大平台
  • 管理咨询公司经营范围包括哪些/优化是什么意思?
  • 芜湖县住房建设局网站/最有吸引力的营销模式
  • WordPress更改角色插件/网站推广和优化系统