延时队列

一。应用场景

  1. 订单成功后,在30分钟内没有支付,自动取消订单
  2. 外卖平台发送订餐通知,下单成功后60s给用户推送短信。
  3. 如果订单一直处于某一个未完结状态时,及时处理关单,并退还库存
  4. 淘宝新建商户一个月内还没上传商品信息,将冻结商铺等
    ……

上边的这些场景都可以应用延时队列解决。

二。实现方式

  2.1 定时任务遍历数据库

  2.2 redis zset

  2.3 rocketsMq 的延时队列

三。具体实现

  3.1 定时任务遍历数据库

    把我们需要做延时执行的任务信息写入下面表

id orderId  expire_time status
1 t90121212 2022-02-15 10:00:00 1
2 t90121213 2022-02-15 11:00:00 0
3 t90121214 2022-02-15 12:00:00 0

  定时的执行扫描sql来取消订单,status 0表示未执行,1是已执行

  

查看代码

SELECT
	id,
	order_id 
FROM
	order_job 
WHERE
	expire_time < now( ) 
	AND `status` =0

优点:简单,开发周期短。

缺点:数据量大的时候可能有性能问题,导致延时任务执行时间过长导致未在规定时间完成业务操作,也很容易造成慢查

 3.2 redis zset

   利用 zset的score特性,把我们任务的到期时间转成unix时间戳写成score,形成一个排序队列。消费的时候使用zrangeWithScores获取优先级最高的(最早开始的的)任务。注意,zrangeWithScores并不是取出来,只是看一下并不删除,看一下时间是否到期,如果到期执行任务,然后删除元素,从而达到延时执行的效果。

2.png

通过zadd命令向队列delayqueue中添加元素,并设置score值表示元素过期的时间;向delayqueue添加三个order1、order2、order3,分别是10秒、20秒、30秒后过期。

zadd delayqueue 3 order3


消费端轮询队列delayqueue,将元素排序后取最小时间与当前时间比对,如小于当前时间代表已经过期移除key。

/**
 * 消费消息
 */
public void pollOrderQueue() {

    while (true) {
        Set<Tuple> set = jedis.zrangeWithScores(DELAY_QUEUE, 0, 0);

        String value = ((Tuple) set.toArray()[0]).getElement();
        int score = (int) ((Tuple) set.toArray()[0]).getScore();

        Calendar cal = Calendar.getInstance();
        int nowSecond = (int) (cal.getTimeInMillis() / 1000);
        if (nowSecond >= score) {
            jedis.zrem(DELAY_QUEUE, value);
            System.out.println(sdf.format(new Date()) + " removed key:" + value);
        }

        if (jedis.zcard(DELAY_QUEUE) <= 0) {
            System.out.println(sdf.format(new Date()) + " zset empty ");
            return;
        }
        Thread.sleep(1000);
    }
} 

这种使用方法,存在一个问题就是并发,如果两个并发线程同时拿到需要延迟的任务,然后执行,最后删除,重复执行。

3.3 rocketMq 重点介绍下(生产中使用

   RocketMQ延迟队列的核心思路是:所有的延迟消息由producer发出之后,都会存放到同一个topic(SCHEDULE_TOPIC_XXXX)下,不同的延迟级别会对应不同的队列序号,当延迟时间到之后,由定时线程读取转换为普通的消息存的真实指定的topic下,此时对于consumer端此消息才可见,从而被consumer消费。

 

注意:RocketMQ不支持任意时间的延时,只支持以下几个固定的延时等级
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

下面介绍下使用方法,只要在发送消息的时候指定delaylevel即可达到延时的功能。

//Instantiate with a producer group name.
DefaultMQProducer producer = new
        DefaultMQProducer("please_rename_unique_group_name");
// Specify name server addresses.
producer.setNamesrvAddr("localhost:9876");
//Launch the instance.
producer.start();
for (int i = 0; i < 10; i++) {
    //Create a message instance, specifying topic, tag and message body.
    Message msg = new Message("TopicTest" /* Topic */,
            "TagA" /* Tag */,
            ("Hello RocketMQ " +
                    i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
    );
    //Call send message to deliver message to one of brokers.
    msg.setDelayTimeLevel(5);
    SendResult sendResult = producer.send(msg);
    System.out.printf("%s%n", sendResult);
}
//Shut down once the producer instance is not longer in use.
producer.shutdown();

以上是我知道的几种延时队列。比较好理解的方案。

posted @ 2022-02-15 14:21  qiushui  阅读(234)  评论(0)    收藏  举报