思考延时队列

更多请移步我的博客

背景

项目中存在以下场景需要延迟触发一些事件:

  1. 订单在未支付状态下30分钟后自动关闭;
  2. 订单超过15天未主动确认收货需要自动确认收货;
  3. 商品价格需要在不同的时间段生效不同的价格方案等。

以上场景下需要有一个相对平台化的服务来满足,而不必每个项目自己做定时任务去进行轮询。

解刨延迟/定时任务

构成一个任务有两个要素:执行时间;执行逻辑。对任务规划者而言,并不关心任务执行逻辑,规划者只要在既定的时间触发该任务,但既然作为一个规划者,就必须具备任务的基本维护能力:新增,删除/取消,到期,查找。

那么一个想要实现规划者就必须考虑两件事:1.怎样即时发现时间到期;2.怎样提高任务的维护效率,即怎么存储任务来保证对任务的高效操作。

本文只关注延时队列中对任务的基本规划能力的实现方式,不涉及延时系统的设计讨论,系统层面的话题太大了。

Rocketmq延迟队列实现

Rocketmq的定时队列通过一个叫做“SCHEDULE_TOPIC_XXXX”的Topic来实现,这个Topic用来处理需要被延迟发送的消息。在Rocketmq中延迟消息被分为几个延迟级别,每个延迟级别分别对应“SCHEDULE_TOPIC_XXXX”Topic下一个延迟队列,默认延迟级别为:“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”。在Broker启动时,会启动相对应队列的线程来处理各个延迟队列的延迟消息。

盗用艾瑞克一次分享中的图来直观感受下延迟队列的实现。
rmqDelayQueue.png

Rocketmq处理通过消息体的扩展字段DELAY来区分Producer是否投递的是延迟消息,如果DELAY大于0,即确定是延迟消息,Broker会备份源消息的topic和queueId,并将其替换为对应延迟队列的信息,然后将修改后的消息落盘到commitLog,DefaultMessageStore#ReputMessageServiceReput线程将消息分发至对应Topic的消息队列(messageQueue),延迟队列被ScheduleMessageService消费,延迟消息到期后会被封装为一个新消息(恢复其源Topic及queueId等信息)再次走消息的投递流程到commitLog,然后被Reput到最初要投递的队列,在整个过程中ScheduleMessageService同时扮演了Consumer和Producer的角色,区分好这两种角色后再来看ScheduleMessageService这段代码会清楚不少。下面列出的代码有所删减改,目的是为了表达核心逻辑。

// ScheduleMessageService

public void start() {
   
    if (started.compareAndSet(false, true)) {
   
        this.timer = new Timer("ScheduleMessageTimerThread", true);
        // 给不同级别的队列启动对应的任务线程
        for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
   
            Integer level = entry.getKey();
            Long timeDelay = entry.getValue();
            Long offset = this.offsetTable.get(level);
            if (null == offset) {
   
                offset = 0L;
            }

            if (timeDelay != null) {
   
                this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
            }
        }

        this.timer.scheduleAtFixedRate(new TimerTask() {
   
            @Override
            public void run() {
   
                try {
   
                	// 定时持久化消费进度
                    if (started.get()) ScheduleMessageService.this.persist();
                } catch (Throwable e) {
   
                    log.error("scheduleAtFixedRate flush exception", e);
                }
            }
        }, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
    }
}

/**
* ScheduleMessageService的内部类
*/
class DeliverDelayedMessageTimerTask extends TimerTask {
   
	public void executeOnTimeup() {
   
		// 找到对应的队列
        ConsumeQueue cq =
            ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(SCHEDULE_TOPIC,
                delayLevel2QueueId(delayLevel));

        long failScheduleOffset = offset;
		// 如果队列不存在,DELAY_FOR_A_WHILE后重新尝试。todo: 什么情况下会出现队列为null呢???
        if (cq == null) {
   
			ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel,
            	failScheduleOffset), DELAY_FOR_A_WHILE);
            return;
        }

        // 从指定位置拉取队列中的可用消息
        SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
        if (bufferCQ != null) {
   
            try {
   
                long nextOffset = offset;
                int i = 0;
                ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
        
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值