RocketMQ 任意时间维度的延迟消息(秒级)

RocketMQ 任意时间维度的延迟消息(秒级)

基于开源版本固定等级的延迟消息实现原理的基础上进行扩展,将所有维度的延迟消息封装成任务添加到时间轮上,通过时间轮固定周期的扫描,检测任务是否到执行时间, 进而达到任意等级维度的延迟消息。

1、修改RocketMQ client消息体结构, 新增一个属性来标识该消息的定时投递的时间。

// org.apache.rocketmq.common.message.Message#setDelayTimeAtTime   
public void setDelayTimeAtTime(long time){
        // 延迟消息落盘问题 对延迟时间进行限制
       // 精确到s级
       this.putProperty(MessageConst.PROPERTY_DELAY_ARBITRARILY_TIME_LEVEL, String.valueOf(time));
   }

2、在消息投递到Broker端进行存储时,判断消息是否为定时消息。如果是定时消息,则将消息进行转换。

if (msg.getDelayTimeAtTime() > 0){
  	// 该消息是定时消息 
     String timeTopic = TopicValidator.RMQ_TIME_TOPIC; //定时消息内置Topic
      // 消息真实Topic 存储到消息扩展字段中
      MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
  		// 消息真实queueId 存储到消息扩展字段中
      MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID,String.valueOf(msg.getQueueId()));
      msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
  		// 将消息的目标topic 修改为内置topic
      msg.setTopic(timeTopic);
  		// 目前暂时设置一个队列, 所有定时消息均放置到一个队列上。
      msg.setQueueId(0);
   }

3、消息保存到CommitLog上之后, 会有org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService#doReput任务对刚刚保存到CommitLog上的消息构建ConsumeQueue、index文件。此处也会对TopicValidator.RMQ_TIME_TOPIC构建ConsumeQueue(原有逻辑,此处不用改变)

4、轮询扫描(3)步骤生成的ConsumeQueue。如果有数据,则根据偏移量从CommitLog上获取消息真实内容,基于消息的定时时间将消息任务放入时间轮。

		public void run() {
            long currentTime = System.currentTimeMillis();
            // 根据偏移量 读取consumerQueue, 将consumerQueue中未消费的数据 加载到时间轮中
            // 默认一个队列
            ConsumeQueue consumeQueue = FixedTimeMessageService.this.defaultMessageStore.findConsumeQueue(TopicValidator.RMQ_TIME_TOPIC, 0);
            this.offset = Optional.ofNullable(FixedTimeMessageService.this.queueOffsetTable.get(0)).orElse(0L);
            if (consumeQueue == null){
                FixedTimeMessageService.this.scheduleNextTimerTask(this.offset);
                return;
            }
            // 读取 consumerQueue 队列, 加载到时间轮中
            SelectMappedBufferResult mappedBufferResult = consumeQueue.getIndexBuffer(this.offset);
            if (mappedBufferResult == null){
                FixedTimeMessageService.this.scheduleNextTimerTask(this.offset);
                return;
            }
            int i = 0;
            for (; i < mappedBufferResult.getSize() && isStarted(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE){
                // 从commmitLog加载消息 加载消息具体内容到时间轮
                long offsetPy = mappedBufferResult.getByteBuffer().getLong();
                int sizePy = mappedBufferResult.getByteBuffer().getInt();
                long tagCode = mappedBufferResult.getByteBuffer().getLong();
                MessageExt messageExt = FixedTimeMessageService.this.defaultMessageStore.lookMessageByOffset(offsetPy, sizePy);
                if (messageExt == null){
                    System.err.println("load message from commitlog error, offsetPy: " + offsetPy + ", sizePy" + sizePy);
                    continue;
                }
                long delayTimeAtTime = messageExt.getDelayTimeAtTime();
                long delayTime =  delayTimeAtTime - currentTime;
                System.out.println("delayTime : " + delayTime + ", delayTimeAtTime: "+ delayTimeAtTime + ", currentTime:" + currentTime);
                if (delayTime <= -1000){
                    continue;
                }
                if (delayTime <= 0){
                    delayTime = 1;
                }
                FixedTimeMessageService.this.addTask(messageExt.getQueueOffset(), messageExt.getQueueId(), delayTime);
            }
            long queueOffset = this.offset + (i / 20);
  					// 此时只是提交任务到时间轮, 此时刷新消费进度会有问题, 如果此时持久化进度文件,并宕机 则重启后会导致消息丢失。
            FixedTimeMessageService.this.queueOffsetTable.put(0, queueOffset);
            FixedTimeMessageService.this.doImmediate(queueOffset);
        }

5、时间轮任务。在时间轮检测到时间到期之后,会触发执行TimerTask,此时根据参数将原消息从CommitLog中取出,然后转换消息topic为真实的topic,queueId为真实的queueId,并将原消息的定时参数清除,重新投递到CommitLog,此时消息便对消费者可见。

	public class FixedTimeTask implements TimerTask {

        private long queueOffset;
        private int queueId;

        public FixedTimeTask(long queueOffset, int queueId){
            this.queueOffset = queueOffset;
            this.queueId = queueId;
        }

        @Override
        public void run(Timeout timeout) throws Exception {
            System.out.println("[FixedTimeTask] execute task >>>> ");
            // 获取 ConsumeQueue
            ConsumeQueue consumeQueue = FixedTimeMessageService.this.defaultMessageStore.findConsumeQueue(TopicValidator.RMQ_TIME_TOPIC, queueId);
            if (consumeQueue == null){
                // redo
                System.err.println("[FixedTimeTask] consumer queue is null, queueId: " + queueId);
                return;
            }
            // 获取该offset的 consumerQueue 数据,之后的不用管
            SelectMappedBufferResult bufferResult = consumeQueue.getIndexBuffer(this.queueOffset, ConsumeQueue.CQ_STORE_UNIT_SIZE);
            if (bufferResult == null){
                // redo
                System.err.println("[FixedTimeTask] bufferResult is null, queueOffset: " + queueOffset);
                return;
            }
            // 此时ByteBuffer 会获取很多
            long offsetPy = bufferResult.getByteBuffer().getLong();
            int sizePy = bufferResult.getByteBuffer().getInt();
            // 获取message
            MessageExt msgExt = FixedTimeMessageService.this.defaultMessageStore.lookMessageByOffset(offsetPy, sizePy);
            if (msgExt == null){
                System.out.println("[FixedTimeTask] load message offsetPy: " + offsetPy + " sizePy:" + sizePy);
                return;
            }
            // 转换消息 topic 换成真实topic
            MessageExtBrokerInner messageExtBrokerInner = FixedTimeMessageService.this.messageTimeup(msgExt);
            // 投递到commitLog
            System.out.println("[MessageExtBrokerInner] topic: " + messageExtBrokerInner.getTopic() + ", queueId: " + messageExtBrokerInner.getQueueId());
            CompletableFuture<PutMessageResult> asyncPutMessage =
                    FixedTimeMessageService.this.defaultMessageStore.asyncPutMessage(messageExtBrokerInner);
            PutMessageResult putMessageResult = asyncPutMessage.get();
            System.out.println("[FixedTimeTask] put message result " + putMessageResult);
           // 此时应记录消费的进度。但需要考虑一个问题 比如offset 100 的消息定时时间是2022-05-10 23:00:00
           // 但是 offset 200的消息定时时间是2022-05-10 19:00:00,那么会是offset 200的消息先消费 但此时如果直接将消费进度修改为200,会有问题
            // 可参考 集群并发消费模式的 消费进度上报机制。 treeMap
        }
    }

5、定时持久化消费进度,重启时加载消费进度。Broker启动时启动定时消息线程。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值