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

从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号传给方法。其中涉及到日志记录、监控数据判断是否是热点数据。

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

  • 9
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值