【业务场景实战】如何优雅地进行缓存预热?

news2024/12/26 18:15:19

从Java基础到中间件再到微服务,我们学了这么多,但遇到真实项目的时候,还是不会根据所学知识,对项目进行改造;或者太久不用早已忘记。学会用才是走得更远!

缓存穿透、雪崩,大家都不陌生,但其中针对的解决方案,有自己手动去实现过吗?

下面带大家去实现一下!

一、问题和解决方案

场景:**缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。** 缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。

为了保证非热点数据不占用太多内存空间,我们设置了逻辑过期时间。

但是如果热点数据出现过期就会造成缓存穿透、雪崩这些问题。为了解决这些问题,我们需要对已经过期或者将要过期的数据进行缓存重建。

重新导入数据到Redis中,并且重新设置逻辑过期时间。缓存重建需要对一些热点数据进行预热。之前我是这么预热的

@Test
    void testSaveShop() {
        //测试id=1,时间10s
        redisUtils.saveShop2Redis(1L, 120L);
    }

这样一个一个的写入id号,效率着实有点太慢了,而且万一不记得了,程序就会出现报错了。

针对缓存重建问题,我这里介绍使用缓存预热的两种方法来实现

二、缓存预热两种方案

1、定时任务

使用`@EnableScheduling`开启定时任务

1)获取ID列表


我们在mapper上创建方法,获取数据id号

@Select("SELECT id FROM tb_shop")
    List<Integer> selectAllIds();


2)缓存重建逻辑

这段缓存重建的逻辑:
先根据传入的id号从数据库中获取值,
封装逻辑过期时间和数据,最后将数据进行写入

//缓存重建
public void saveShop2Redis(Long id, Long expireSecond) {
    String key = CACHE_SHOP_KEY + id;
    //1、查询店铺数据
    Shop shop = shopMapper.selectById(id);
    //2、封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSecond));
    redisData.setData(shop);
    //3、写入redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}


3)定时任务缓存类


因为不需要特别复杂的逻辑,所以我这里就使用最为简单的spring自带的定时任务。
创建缓存重建定时任务类

- 首先开启定时任务注解

- 调用shopMapper方法,查询所有id号,遍历

- 将遍历的id号传入saveShop2Redis方法中,并设置逻辑过期时间60秒

- 日志打印输出

@Component

@Slf4j

public class CachePreheatTask {

    @Resource
    private RedisUtils redisUtils;
    @Resource
    private ShopMapper shopMapper;

    // 执行缓存预热任务的方法
    @Scheduled(cron = "0 19 9 * * ?")
    public void preheatCache() {
        // 执行缓存预热逻辑
        List<Integer> selectAllIds = shopMapper.selectAllIds();
        for (Integer allId : selectAllIds) {
            //测试id=1,时间10s
            redisUtils.saveShop2Redis(Long.valueOf(allId), 60L);
            log.debug("缓存数据预热成功,id为:{}" ,allId);
        }
    }
}

这里的表达式:cron = "0 19 9 * * ?"
表示的是每天上午9点19执行定时任务
调用 `selectAllIds`方法,把数据库表中的id查询出来,然后进行遍历
再利用for循环,把每次查询出的id传给`saveShop2Redis`方法进行缓存重建
为了方便测试,我设置逻辑过期时间为60秒。

`@Scheduled`注解是Spring框架中用于创建定时任务的注解,它有三个不同类型的参数:`cron`、`fixedDelay`、`fixedRate`,分别用于不同的定时任务需求。

1、 `cron`参数:用于指定一个cron表达式,可以精确控制任务的执行时间。cron表达式是一个字符串,包含六个或七个空格分隔的时间字段,用于指定秒、分、时、日、月、周几等时间点。例如,`"0 * * * * ?"`表示每分钟执行一次。

2、 `fixedDelay`参数:用于指定任务执行结束后到下一次任务开始的间隔时间,单位为毫秒。即任务的执行周期是任务结束后延迟指定的时间后再执行。

   例如,`@Scheduled(fixedDelay = 1000)`表示任务执行结束后延迟1秒后再执行。

3.、`fixedRate`参数:用于指定任务开始执行后到下一次任务开始的间隔时间,单位为毫秒。即任务的执行周期是任务开始后固定的时间间隔再执行。例如,`@Scheduled(fixedRate = 1000)`表示任务开始后每隔1秒执行一次。

这些参数可以根据实际需求来选择,`cron`表达式适用于需要精确控制执行时间的场景,`fixedDelay`适用于任务执行时间不固定的场景,`fixedRate`适用于固定频率执行任务的场景。

表达式
表达式意义
每隔5秒钟执行一次*/5 * * * * ?
每隔1分钟执行一次0 * /1 * * * ?
每天1点执行一次0 0 1 * * ?
每天23点55分执行一次0 55 23 * * ?


这样就能达到我想要的效果,可以随便设置定时任务的执行时间,这样就可以提前进行预热了。

2、消息队列


下面再介绍一种可以进行数据预热的方式——消息队列。

思考一下:我们的诉求是什么?

我们需要将数据进行预热,那我们是不是要拿到数据的id号。

拿到了id号呢,我们怎么让程序自动地去执行这段重建逻辑呢?

对的,使用消息队列,把id号传给消息队列,然后在项目启动的时候,让生产者去发送这个消息。消费者拿到消息之后,就会去执行重建的逻辑了。

这里一些关于MQ配置什么的我就不写了,都是固定的,

1)生产者代码

@Component
public class MyMessageProducer {
    @Resource
    private RabbitTemplate rabbitTemplate;
    // 向指定交换机发送消息
    public void sendMessage(String exchange, String routingKey, String message) {
        //将消息发送到指定的交换机和路由键
        rabbitTemplate.convertAndSend(exchange,routingKey,message);

    }
}

2)消费者代码

@Component
@Slf4j
public class MyMessageConsumer {

    @Resource
    private RedisUtils redisUtils;

    /**
     * 接收消息的方法
     *
     * @param message
     * @param channel
     * @param deliveryTag
     */
    //使用@SneakyThrows注解简化异常处理
    @SneakyThrows
    //使用该注解指定程序要监听的队列,,并设置消息的确认机制为手动
    @RabbitListener(queues = {"hmdp_queue"}, ackMode = "MANUAL")
    //@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag 用于从消息头中获取投递标签deliveryTag
    //在mq中,每条消息都会被分配一个唯一投递标签,用于标识该消息在通道中的投递状态和顺序,使用该注解可以从消息头中获取该投递标签,并将其赋值给deliveryTag参数,
    public void receiveMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
        long shopId = Long.parseLong(message);
        redisUtils.saveShop2Redis(shopId, 60L);
        log.info("收到消息,传入的shopId为:{}", message+",缓存数据预热成功!");
        //手动确认消息,消息确认标志设置为false,消息才能被确认
        channel.basicAck(deliveryTag, false);
    }
}


3)创建队列交换机


在程序执行前创建好交换机和对列

public class biInitMain {
    public static void main(String[] args) {
        try {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("192.168.88.130");
            factory.setPort(5672);
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();

            String EXCHANGE_NAME = "hmdp_exchange";
            channel.exchangeDeclare(EXCHANGE_NAME, "direct");

            // 声明一个队列,并且设置持久化消息
            String queueName = "hmdp_queue";
            String ROUTING_KEY="hmdp_routingKey";
            channel.queueDeclare(queueName, true, false, false, null);
            //队列绑定交换机,routing_key用于指定消息应该发送到哪个队列。
            channel.queueBind(queueName, EXCHANGE_NAME, ROUTING_KEY);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


4)发送消息

用于获取热点id并将其发送到消息队列。这个程序应该只执行一次,以确保不会重复发送相同的id。

`@PostConstruct`注解会让项目启动时初始化这段代码,被执行一次。这样消息也就被发送给消费者了,缓存重建的逻辑也就执行成功了

@PostConstruct
    public void init() {
        myMessageProducer.sendMessage("hmdp_exchange"
,"hmdp_routingKey",String.valueOf(1L));
       
    }


看看控制台

其实代码到这里还是有点小问题的,细心的兄弟应该看出这里的问题了。

对的,之前使用定时任务,获取的是所有数据的id,获取的是所有的数据。
而这次消息队列改造,传入的是一个固定的id值。其实这里应该需要去获取一些热点数据id,再将这些id号传给方法。其中涉及到日志记录、监控数据判断是否是热点数据。

到这里我的缓存预热就结束了,其实就类似于项目的一个小优化的一样
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2046881.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Linux操作系统】——Centos7下安装python3.8

1.Centos7下安装python3.8 1.1 安装依赖包 yum install gcc openssl-devel bzip2-devel libffi-devel zlib-devel ncurses-devel sqlite-devel readline-devel readline-devel make在CentOS 7上安装Python时&#xff0c;‌需要安装一些必要的依赖包以确保Python的正常编译和运…

HarmonyOS NEXT - 项目基础框架的搭建

demo 地址: https://github.com/iotjin/JhHarmonyDemo 代码不定时更新&#xff0c;请前往github查看最新代码 HarmonyOS NEXT - 项目基础框架的搭建 序前置工作项目的目录结构主界面实现BaseTabBar代码实现子页面实现路由跳转登录页面和主页面切换登录实现退出登录 序 项目基于…

Spark-环境启动

一、概览 从start-all.sh开始捋&#xff0c;一直捋到Master、Worker的启动并建立通信 二、宏观描述 Master端 1、start-all.sh调用start-master.sh启动Master 2、执行org.apache.spark.deploy.master.Master中main方法 3、通过工厂模式创建RpcEnv子类NettyRpcEnv a、创建…

viper配置文件读取管理库 一个支持12种文件类型,5种远程协议的配置文件管理和加载工具库 使用方法示例

viper是一个不错的配置文件管理库&#xff0c; 他支持的配置文件类型依次有 "json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv&quo…

java:实现简单的验证码功能

效果 实现思路 验证码图片的url由后端的一个Controller生成&#xff0c;前端请求这个Controller接口的时候根据当前时间生成一个uuid&#xff0c;并把这个uuid在前端使用localStorage缓存起来&#xff0c;下一次还是从缓存中获取。 Controller生成验证码之后&#xff0c;把前…

Qt 学习第六天:页面布局

如何设计页面&#xff1f; 有个类似沙盒模式的玩法&#xff0c;Qt Widget Designer可以更好的帮助我们设计页面 点击.ui文件进入 右上方可以看到四种常见的布局&#xff1a; 四种布局 &#xff08;一&#xff09;水平布局horizontalLayout&#xff1a;QHBoxLayout H 是 hori…

c++实现B树(上)

哈喽啊&#xff01;好久不见&#xff0c;甚是想念&#xff01;失踪人口要回归了&#xff0c;时隔一个多月小吉我终于要更新blog了&#x1f389;。在停更的一个多月中&#xff0c;小吉也有在好好学习提升自己&#xff0c;立志给大家呈现好文章。  现在让我们进入正题吧&#xf…

初识C++:开启C++之旅

目录 1.C的第一个程序 2.namesapce命名空间域 2.1namespace的意义 2.2.2namespace的定义 2.3命名空间的使用 3.C输入/输出 4.缺省参数 5.函数重载 6.引用 6.1引用的特性 6.2引用的使用 1.C的第一个程序 c版本&#xff1a; #include<iostream>using std::cout…

kali安装wechart

前言&#xff1a; 突发奇想想在kali安装个wechart&#xff0c;试了下网上的很多&#xff0c;玩坏了一个虚拟机算是找到了一个不错的方法&#xff0c;这里记录下&#xff0c;防迷路 基础配置&#xff1a; 首先修改源&#xff1a; vim /etc/apt/sources.list 注释默认配置&…

EasyCVR视频汇聚平台:打造全栈视频监控系统的基石,解锁可视化管理与高效运维

随着科技的飞速发展&#xff0c;视频监控已成为现代社会不可或缺的一部分&#xff0c;广泛应用于社区、公共场所、工业领域等多个场景。EasyCVR视频汇聚平台&#xff0c;作为一款高性能的视频汇聚管理平台&#xff0c;凭借其强大的视频处理、汇聚与融合能力&#xff0c;在构建全…

centos8 安装zookeeper

1&#xff1a;下载 zookeeper官网 解压&#xff1a;tar -zxvf apache-zookeeper-3.6.3.tar.gz 修改自己想要的文件目录 mv apache-zookeeper-3.6.3 zookeeper_3.6.3 备份一下 配置文件 cp zoo_sample.cfg zoo.cfg vim zoo.cfg 编辑日志文件和端口号

nginx实战演练

目录 一.Nginx架构和安装&#xff08;未完待续&#xff09; <1>.Nginx概述 <2>.Nginx架构和进程 <3>.Nginx模块 <4>.Nginx安装(编译安装) 二.Nginx基础配置 <1>.关闭debug <2>.将nginx软件添加到环境变量 <3>.开机自启动脚…

EmguCV学习笔记 VB.Net 2.5 Mat类、Matrix类和Image类的相互转换

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 EmguCV学习笔记目录 Vb.net EmguCV学习笔记目录 C# 笔者的博客网址&#xff1a;VB.Net-CSDN博客 教程相关说明以及如何获得pdf教…

基于Vue的MES生产制造执行系统

TOC springboot307基于Vue的MES生产制造执行系统 第1章 绪论 1.1 选题动因 到现在为止&#xff0c;互联网已经进入了千家万户&#xff0c;最普通的平民百姓也有属于自己的智能设备&#xff0c;计算机各种技术的储备也是相当的丰富&#xff0c;并且实现也是没有难度&#xf…

VSCode配置ssh免密连接远程服务器

我配置了免密设置(Windows利用ssh免密码登录Linux)&#xff0c;git bash已经能够正常连接了&#xff0c;但是vscode还是不行&#xff0c;很奇怪。 VSCode报错信息&#xff1a; [17:55:50.360] SSH Resolver called for "ssh-remote106.52.2.19", attempt 5, (Recon…

《机器学习》——运用OpenCV库中的KNN算法进行图像识别

文章目录 KNN算法的简单介绍下载OpenCV库实验内容实验结果完整代码自己手写数字传入模型中测试 KNN算法的简单介绍 一、KNN算法的基本要素 K值的选择&#xff1a;K值代表选择与新测试样本距离最近的前K个训练样本数&#xff0c;通常K是不大于20的整数。K值的选择对算法结果有重…

电压检测之比较电路

设计这款电路主要是本人在锂电池充电电路中挖了一个坑&#xff0c;对电源显示芯片的数据手册内容撰写不够详细的不好感受&#xff0c;所以自己根据比较电路的思想设计出了电压检测并反馈的电路&#xff0c;亦在提供一种电压检测的思想不需要借助ADC采集&#xff0c;在电路硬件上…

基于hive的海鲜交易数据分析系统设计与实现【hadoop、Flask、某东爬虫、sqoop、flume、mysql、hdfs】商品可换

文章目录 有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主项目介绍研究背景国内外研究现状研究目的研究意义 关键技术理论介绍数据采集及预处理数据采集字段介绍数据预处理hadoop集群搭建及实现过程hive建表hive大数据分析 可视化展示店铺维度画像分…

AR 眼镜之-开关机定制-实现方案

目录 &#x1f4c2; 前言 AR 眼镜系统版本 开关机定制 1. &#x1f531; 技术方案 1.1 技术方案概述 1.2 实现方案 1&#xff09;开机 Logo 2&#xff09;开机音效 3&#xff09;开机动画 4&#xff09;关机动画 5&#xff09;关机弹窗 2. &#x1f4a0; 开机 Logo…

C++笔试题汇总

C笔试题汇总记录 一、概述二、概念分类1. 结构体1. C 和 C 中 struct 有什么区别&#xff1f;2. C中的 struct 和 class 有什么区别&#xff1f; 2. 类相关1. 类的大小1. 空类的大小2. 一般非空类大小3. 有虚函数类4. 有虚函数类的继承5. 只有虚函数6. 静态数据成员 2. C的三大…