如何优雅地进行缓存预热?

一、问题和解决方案

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

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

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

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

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

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

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

二、缓存预热两种方案

1、定时任务

使用@EnableScheduling开启定时任务

1)获取ID列表

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

text
复制代码
@Select("SELECT id FROM tb_shop") List<Integer> selectAllIds();

2)缓存重建逻辑

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

text
复制代码
//缓存重建 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秒

  • 日志打印输出

text
复制代码
@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框架中用于创建定时任务的注解,它有三个不同类型的参数:cronfixedDelayfixedRate,分别用于不同的定时任务需求。

  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号传给消息队列,然后在项目启动的时候,让生产者去发送这个消息。消费者拿到消息之后,就会去执行重建的逻辑了。

mermaid
复制代码
graph TD 消息shopID号 --> 消息队列 --> 发送消息 --> 消费消息 --> 缓存重建

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

1)生产者代码

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

2)消费者代码

text
复制代码
@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)创建队列交换机

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

text
复制代码
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注解会让项目启动时初始化这段代码,被执行一次。这样消息也就被发送给消费者了,缓存重建的逻辑也就执行成功了

text
复制代码
@PostConstruct public void init() { myMessageProducer.sendMessage("hmdp_exchange","hmdp_routingKey",String.valueOf(1L)); // 启动项目时初始化bloomFilter //bloomFilter = bloomFilterManager.create("bloomShopID", expectedInsertions, falseProbability); //List<Integer> list = shopMapper.selectAllIds(); //for (Integer shopId : list) { // bloomFilter.add(BLOOM_FILTER_SHOP + shopId); //} }

看看控制台

image.png

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

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

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

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
苦行修士
作者分享
26届转正暑假实习 蚂蚁一面 昨天晚上7点收到了蚂蚁的电话面试邀请,有点突然,他刚开始打电话给我我没接到,然后打回去他又没收到,最后还是他打过来😂,我以为我笔试都没做,应该不会收到面试。 因为忘记录音,所以只能凭记忆复盘一下,大伙凑合看吧。 1、自我介绍 2、你的学历是怎么回事?我看你放的博客上面有一篇文章,你2024年变成本科生,现在就大三了? 额,我其实是专升本,读了三年专科,然后专科毕业直接到本科大三,不是从本科大一开始。不是那种函授本科哈,我是正二八经的全日制本科大学生,我现在还在学校呢(说的我自己都快笑了😂)。 (这把真的是被自己坑了,我简历上写的是四年本科,然后博客上有时候发一些个人感想,不小心透露了信息,还好他并不介意我的学历) 3、我看你上面有两个项目,关于智能bi项目,描述一下 你的系统预设是怎么做的?放在哪里的? 你用的是什么ai模型? 你是怎么通过ai分析文件和诉求来实现的数据可视化? 你用的ai模型是spring框架自带的吗? 你可以去了解一下springAI框架 你简历上对ai接口做了二次限流,利用hash表存储ai接口调用次数做限流是怎么回事? (这题没答好,其实redisson提供了分布式的一次限流是基于技术上的限流,hash存储ai接口调用次数是基于业务层面的二次限流) 你的限流参数设置的是多少?怎么做的压测?压测结果?(这个是真忘了) 分布式限流redisson里面用的限流算法是什么? 我:是令牌桶算法,它是—— 他:这个不用跟我扩展 你是怎么压缩数据的,减小单次token输入量的? (这题我在项目上是真没注意到,只能说是通过easyexcel提供的方法实现的压缩) 4、你这个api开放平台是怎么做的? 你里面用了一个api网关是吧 你的api签名校验是怎么实现的? 你的这个api开放平台的实现流程,aksk的作用? 说说为啥用Dubbo? Dubbo和SpringCloud的区别? Dubbo协议用的是什么协议? 5、你还用过其他的ai吗? 我:我用过目前比较热门的deepseek,它比星火模型更加好用,因为星火模型它的回答有些是错误的,它有时候会按照我的思路去回答问题,对我提出的疑问回答的并不是我想要的那种。但deepseek不一样 他:那你都问过什么问题,做了对比呢? 我:比如关于死信队列和延时队列 他:那你讲讲死信和延迟队列的区别 6、除了这上面的两个项目,有其他印象比较深刻的项目吗? 我:我做过一个秒杀商城项目,虽然这是b.站上烂大街的项目,但他符合技术使用的特殊应用场景,比较经典,还是很值得一做的。 他:说说你做的这个秒杀商城项目? 他:这个分布式定时任务是怎么做的? 我:他是将秒杀商品分成不同的场次,使用分布式任务调度进行任务的分片,然后在秒杀时系统会校验当前的时间是否在秒杀范围之内。(说实话关于分布式定时任务怎么实现的分片我也有点不记得了,其实只需要配置分片参数即可,和分库分表差不多,只是这个项目我没想到他会问,压根没看) 他:…… 8、他让我结合秒杀商城项目做一下智能bi项目的扩展 基于智能bi项目ai接口调用限制,可以再扩展一个支付系统,但因为不好设计,所以没做。因为支付不同的金额对ai接口的调用额度不同,需要设计商品表,订单表,需要用到沙箱支付宝,并且将项目改造为微服务项目,分成四个模块—支付、用户、任务处理、……(难度系数几何倍数增长),架构设计目前还不是很熟。 7、结尾阶段 他:你这个博客做的不错,怎么断更了? 我:额,这段时间太忙,准备八股文啥的,就断更了。我喜欢分享知识,通过写技术博客增强对知识的理解,同时也会学习别人的文章。加上大学生活比较无聊,就想着写一写。 他:我看到有些实习的,会有一些自己的博客,还有小红书的,都挺好的。不要断更啊,继续保持~ 我:好的好的(傻笑)😄😄 他:你还有什么要问的? 我表现怎么样?您有什么建议吗?😅 他: 1、你这个项目其实挺不错的,特别对于目前ai比较火,但你对ai的理解不够,你可以去用一下spring官方提供的springAI框架,然后看看怎么实现的,而不是直接用别人封装好的ai框架。(行吧,原来是我能力不足😭) 2、刚才打电话的时候你语速有点太快了,可能是太紧张了。 有点可惜(这个时候我就知道我寄了🐔) 3、我当时就是看了你的这个项目不错,然后也看了你的博客,觉得有点意思,然后给你发的面试 4、然后他还说我是他26届暑假实习面试的第一个人(真倒霉啊,哥你要是多对比一下,我是不是还看得过去呢😅) 最后他还给了我一些鼓励,果然大厂的面试官就是不一样啊! 总结:这次面试真的有点可惜,面试的时候有点紧张,说话都没经过思考,导致答的并不是很好。下次一定要等个几秒,组织一下语言再回答,就不会出现中途卡壳。中途我卡了好几次,然后好几次都抢了他的话,他还是耐心地等我说完。 还有就是确实对项目和知识的理解还存在不足,还需要再深究一下。 面试时长50分钟,虽然挂了,但感觉还挺好的,面试官态度也挺好,也找到了自己的不足。
18
苦行修士的年终总结
31
这段时间在家,我爸妈又给我谈起了职业规划。我目前已经升本,开学之后就是本科大三了,大三这个阶段,大家经历过的都知道其重要性。 这不,我爸妈又打算给我规划人生了。二老急我自然也是知道,但我是真不想去考公啥的。 我爸经常看手机视频,看到视频上很多人说大三要做好职业规划。他想着现在就业机会难,而且私企很不稳定:加班、裁员、外面消费高(网上不少人贩卖焦虑,虽然有一定道理,但我个人觉得也不完全对);加上他想我考公更稳定一些,还听别人说公务员虽然开始工资低,但福利啥的都很好。 我想学好技术进公司直接就业,并不想考公或者进国企(我已经对考试感到恐惧了[撇嘴][撇嘴])。他觉得我可以学好技术,但学技术同时还要准备考公/进国企,考公的科目和进国企考的有重合的地方,他觉得如果我没有考上公,也不妨碍我进国企,再不行,兜底方案直接进私企(想得太美好了[捂脸][捂脸],感情我是有三头六臂啊!!!)。 我:考公很难的 爸:现在什么都难,你只要难这几年,过来了你就好一辈子。 我:进国企很难的,我学历不够,双一流人家才看得上 爸:普通本科也能上,只要你努力了。 我:我就不能只学技术进私企吗?我的技术要学的东西太多了没时间 爸:别和我提没时间,大家都是一样的,没时间就抽时间。你学的技术做的项目,再学也只停留在初级阶段,和公司企业级的项目没法比,不用占用你这么多时间。 你现在升本了,可以选择的路有很多,你可以选择多条路,大三很关键,你大三要是准备好能直接考上公务员,不比你进私企好? 你二姐,我当初就是没给她做好规划,现在都晚了。 我心想:要你操心的时候不操心,不要你操心的时候瞎操心,什么事都压我身上,她没考上也不能全赖没做好规划啊 ,你凭什么认为你给我做的规划就是对的,到时候要是没考上,还不是又赖我身上。 (我大姐是考编很多年都没上,然后我二姐也是幼师专业今年考编也没考上,他们觉得当初我大姐就是没人指导,考编都不知道要准备什么所以没考上,我爸把她每一年的落榜都当成下一年再战的经验。现在有了她的经验,二姐大学就开始准备一定考的上,结果一样没考上。准备今年下半年报班继续考,如果没考上呢?那就一直考[翻白眼][翻白眼][翻白眼] 我爸妈的简单思维:别和我说难,现在什么不难,过了这关就好了,好一辈子,你说难,好,给你报班,多少钱都无所谓,你没上,就是你自己的问题,你没努力,你摆烂。谁谁谁之前大学没准备,现在工作之后后悔了;谁大一大二就开始准备,一下就上了,现在日子过得多潇洒) 第一个娃翻车了, 哎呀,当初怎么没做好规划呢? 明年再来 第二个娃得有第一个娃的经验了,应该不会翻车 哎,怎么又翻车了? 明年再来 痛定思痛,问问别人怎么搞的,(又偏去问人家父母,像我们农村家庭,有几个人懂这个,还不是听人吹牛) 第三个娃,我一定给你把好关(我真的会谢[汗]) 终于理解什么叫:“关关难过关关过,夜夜难熬夜夜熬”![苦涩][苦涩][苦涩] 我知道这个问题星球里面问烂了,但我还是想找一个能说服自己的理由。 我目前是想只学专业,毕业之后直接找工作,不考公不考研不考编,也不打算考什么国企央企。 但我父母是觉得我可以选择多条路,有机会就去考,但这些都是需要时间的呀,我花这么多时间去做一件很大概率没有回报的东西,我觉得太没意义了,加上我现在已经害怕考试了[捂脸] 考公和进国企,还是只学技术进私企,还是都准备,给自己多条路呢? 希望大家给点建议!
18
实习一个星期,我被开了..........
77
公司需要我熟系若依框架,但我没有人带,看了三天,有很多不理解的地方,希望有兄弟能帮我解答一下,加微信RainbowSea3279
8
下载 APP