延时队列
一。应用场景
- 订单成功后,在30分钟内没有支付,自动取消订单
- 外卖平台发送订餐通知,下单成功后60s给用户推送短信。
- 如果订单一直处于某一个未完结状态时,及时处理关单,并退还库存
- 淘宝新建商户一个月内还没上传商品信息,将冻结商铺等
……
上边的这些场景都可以应用延时队列解决。
二。实现方式
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并不是取出来,只是看一下并不删除,看一下时间是否到期,如果到期执行任务,然后删除元素,从而达到延时执行的效果。

通过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();
以上是我知道的几种延时队列。比较好理解的方案。

浙公网安备 33010602011771号