[mq系列]-🚀5.x延迟消息原理与手动实现

469 阅读12分钟

参考代码:

rocketmq-release-5.1.1

参考文章:

segmentfault.com/a/119000004…

juejin.cn/post/730610…


0.引言

rocketmq5.x中实现了任意时间的定时消息,4.x以前只有设置level匹配固定的延迟等级。核心差异在于rocketmq5.x引入了时间轮timerwheel组件。

然后我简单实现了一个基于内存的时间轮,其处理流程大致为:broker收到消息,判断为定时/延迟消息,包装放入timewheel中,内存中的timerwheel自动轮转,扫描到当前slot时就将消息拆解出来,做真实的投递。但这样做是有问题的,消息不可能一直放在内存中的,这样会大量占据服务器资源,如果延迟消息太多可能会搞垮服务器。

因此我决定参考rocketmq的实现方式,自己实现一套延迟消息机制。文章后面会附上我的代码。


1.rocketmq5.x中时间轮的实现方式

1.1文件部分

rocketmq通过timerwheel文件和timerlog文件来实现定时/延迟消息。两个都是需要本地存储的文件。

timerwheel

单位格式为:delayed_time(8b) + first_pos(8b) + last_pos(8b) + msg_count(4b)

delayed_time也就是时间轮的序列,两个position标识timerlog中的开始和结束的物理偏移量,读取时按从后往前的顺序读,找到这个delayed_time对应的所有消息。

timerlog

timerlog文件类似于consumequeue文件,其存储的单位格式为:

size + prev_pos + next_pos + delayed_time + offset_real + size_real + magic + hash_topic

单位的大小也是固定的。

1.2整体流程

简单总结一下rocketmq是怎么处理定时消息的。

涉及到的线程

  1. TimerEnqueueGetService
  2. TimerEnqueuePutService
  3. TimerDequeueGetService
  4. TimerDequeueGetMessageService
  5. TimerDequeuePutMessageService

涉及到的队列

  1. enqueuePutQueue
  2. dequeueGetQueue
  3. dequeuePutQueue

围绕这几个线程和queue来看,rocketmq的时间轮就非常清晰。

流程如下:

  1. 将普通消息转换为wheel_time消息,写入timer_topic,在PutMessage前处理

    1. 统一时间戳,如果还是设置的level则走老流程
    2. 转换成wheel_time消息,将原本消息的topic,queueid存储为properties,将topic更换为timer_topic,返回
  2. 开启一个TimerEnqueueGetService线程,将timer_topic中的msg读取并构成TimerRequest,投递到enqueuePutQueue中

  3. 开启一个TimerEnqueuePutService线程,处理TimerRequest,将消息放入时间轮或直接进入dequeuePutQueue

  4. 时间轮enqueue逻辑(加入时间轮)

    1. 处理滚动逻辑(这部分还没完全看懂),根据延迟时间定位到对应的slot,将消息顺序写入timerlog
    2. 更新TimerWheel中的slot信息,将指向timerlog的指针对象,包括first_pos和last_pos以及delayed_time等写入。
  5. 时间轮dequeue逻辑(消费时间轮)

    1. 开启TimerDequeueGetService,根据时间找到时间轮中的slot
    2. 根据slot存储的指针,读取timerlog中所有的timerlog单位组装成TimerRequest,将其放入dequeueGetQueue
    3. 开启TimerDequeueGetMessageService从commitlog中读取消息,写入TimerRequest,提交到dequeuePutQueue
    4. 开启TimerDequeuePutMessageService将dequeuePutQueue的消息转换并投递到真实的topic
  • 定时/延迟消息的HA机制

实现思路-整体流程图

image.png

2.时间轮组件

rocketmq的timewheel是通过两个本地文件组成的,其中一个是timeslot数组,另一个是数组里的真实数据。那么可不可以将数组直接存储在内存中呢?这样实现起来会简单一点,当然timeslot的重启recover也是要考虑到的。

我自己实现的消息队列支持1h延迟即可,如果超过一天,采用定时任务或更完善的数据存储机制去实现会更可靠一点。1h的延迟消息所需的分层时间轮slot为60+60=120,比起rocketmq最大支持14天的延迟消息来说,我的timeslot会少很多。总共占用的空间大概2,300byte,不是一个很大的数目,内存空间完全是ok的。

这种方案,即在内存中分别记录secondsTimeWheel,minutesTimeWheel,hoursTimeWheel,启动scan任务进行扫描,每个timewheel[i]里都是一个timeslot,每个timeslot对应到timerlog文件,辅助读取。timerlog文件的读取,rocketmq实现的办法是在timerlog文件内部标识出prev的记录在哪,我也按照这种方式实现。

2.1TimerLog

Timerlog文件中就是一个个的TimerLogUnit,TimerLogUnit如下

public class TimerLogUnit {
	private int realSize;
	private int realOffset;
	private int delayedTime;
	private int prevPos;
}

prevPos可以辅助我们找到当前delayedTime的所有Unit,然后读取出来进行消费。

TimerLog我设计为,前4byte记录当前写入的最大offset,后面就是TimerLogUnit[]数组,每个单位长度固定为16byte,如下

public class TimerLog {
	private File file;
	private MappedByteBuffer mappedByteBuffer;
	private ByteBuffer readBuffer;
	private ByteBuffer writeBuffer;
	private final Object timerLogLock = new Object();
	int mappedSize = 10 * 1024 * 1024;

	public void loadFileInMMap() throws IOException {
		//filePath = BrokerContext.getGlobalProperties().getMqHome() + "/timer/timerlog";
		file = new File(filePath);
		if (!file.exists()) {
			throw new RuntimeException("Timer log file does not exist");
		}
		FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();
		this.mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, mappedSize);
		this.readBuffer = mappedByteBuffer.slice();
		int maxOffset = getMaxOffset();
		this.writeBuffer = mappedByteBuffer.slice();
		writeBuffer.position(maxOffset);
	}

	public TimerLogUnit readSlot(int readOffset) {
		ByteBuffer readBuf = readBuffer.slice();
		readBuf.position(readOffset);
		TimerLogUnit slot = new TimerLogUnit();
		slot.setRealSize(readBuf.getInt());
		slot.setRealOffset(readBuf.getInt());
		slot.setDelayedTime(readBuf.getInt());
		slot.setPrevPos(readBuf.getInt());
		return slot;
	}

	public void writeSlot(TimerLogUnit slot) {
		synchronized (timerLogLock) {
			int maxOffset = this.getMaxOffset();
			writeBuffer.position(maxOffset);
			writeBuffer.putInt(slot.getRealSize());
			writeBuffer.putInt(slot.getRealOffset());
			writeBuffer.putInt(slot.getDelayedTime());
			writeBuffer.putInt(slot.getPrevPos());
			maxOffset += 16;
			writeMaxOffset(maxOffset);
			writeBuffer.position(maxOffset);
		}
	}
}

对maxOffset的操作如下

	public int getMaxOffset() {
		ByteBuffer buf = mappedByteBuffer.slice();
		buf.position(0);
		int offset = buf.getInt();
		if(offset == 0) {
			buf.position(0);
			buf.putInt(4);
		}
		return offset;
	}

	private void writeMaxOffset(int maxOffset) {
		ByteBuffer buf = mappedByteBuffer.slice();
		buf.position(0);
		buf.putInt(maxOffset);
	}

同时还有按prevPos读取TimerLogUnit的函数,readList

	public TimerLogUnit[] readList(TimerWheelSlot timerWheelSlot) {
		int ptr = timerWheelSlot.getLastPos();
		int cnt = 0;
		TimerLogUnit[] timerLogUnits = new TimerLogUnit[timerWheelSlot.getMsgCount()];
		while (cnt < timerWheelSlot.getMsgCount() && ptr != -1) {
			TimerLogUnit timerLogUnit = readSlot(ptr);
			ptr = timerLogUnit.getPrevPos();
			timerLogUnits[cnt] = timerLogUnit;
			cnt++;
		}
		return timerLogUnits;
	}

目前使用的是mmap方式映射文件到内存,即写即刷盘,后面需要优化一下IO操作。

2.2TimerWheel

TimerWheel的设计很简单,wheel中包含当前的current slot idx和slotlist,以及计算给定delay需要放置的slot位置的函数

public class TimerWheel {
	int current;
	TimerWheelSlot[] slotList;

	public int countNextSlot(int delay) {
		AssertUtil.isTrue(delay < slotList.length, "delay is over size");
		int remainSlotCount = slotList.length - current;
		int diff = delay - remainSlotCount;
		if (diff < 0) {
			return current + delay;
		}
		return diff;
	}
}

而TimerWheelSlot则记录当前delay下所有TimerLogUnit在TimerLog中的最大pos,最小pos,以及unit count。

public class TimerWheelSlot {
	private int firstPos = -1;
	private int lastPos = -1;
	private int msgCount = 0;
}

以及我们服务中管理TimerWheel的组件,TimerWheelManager。

TimerWheelManager,我目前的设计是秒+分时间轮,最长延迟时间为3600秒,一方面是我的项目最开始就是以电商场景来写的,可能不需要过长的延迟消息,其次实现更多层级的timerwheel也比较复杂。

public class TimerWheelManager {
	private final Logger logger = LoggerFactory.getLogger(TimerWheelManager.class);
	// 秒钟时间轮
	private TimerWheel secondsTimerWheel;
	// 分钟时间轮
	private TimerWheel minutesTimerWheel;

	// 当前时间
	private long executeSeconds = 0L;

	// lock,待优化
	private final Object writeTimerLock = new Object();
	// 事件发布器
	EventBus eventBus;
	// TimerLog文件
	TimerLog timerLog;

	public void init(EventBus eventBus) {
		secondsTimerWheel = new TimerWheel();
		secondsTimerWheel.setCurrent(0);
		secondsTimerWheel.setSlotList(buildTimerSlot(60));
		minutesTimerWheel = new TimerWheel();
		minutesTimerWheel.setCurrent(0);
		minutesTimerWheel.setSlotList(buildTimerSlot(60));
		timerLog = new TimerLog();
		try {
			timerLog.loadFileInMMap();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		this.eventBus = eventBus;
	}

	private TimerWheelSlot[] buildTimerSlot(int count) {
		TimerWheelSlot[] slots = new TimerWheelSlot[count];
		for (int i = 0; i < count; i++) {
			slots[i] = new TimerWheelSlot();
		}
		return slots;
	}

	public void doScanWheel() {
		Thread thread = new Thread(() -> {
			while (true) {
				try {
					logger.info("current time is :{}", executeSeconds);
					doSecondsScan();
					if (executeSeconds % 60 == 0) {
						doMinutesScan();
					}
					TimeUnit.SECONDS.sleep(1);
					executeSeconds++;
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
			}
		});
		thread.setName("scan-timer-wheel-task");
		thread.start();
	}
	
	public void add(TimerRequest request) {
		
	}

	public void doMinutesScan() {
		
	}

	public void doSecondsScan() {
		
	}
}

详细讲一下核心的三个方法,add,doMinutesScan,doSecondsScan

首先是add方法

	public void add(TimerRequest request) {
		int delayTime = request.getDelayTime();

		TimerLogUnit timerLogUnit = new TimerLogUnit();
		timerLogUnit.setDelayedTime(delayTime);
		timerLogUnit.setRealOffset(request.getRealOffset());
		timerLogUnit.setRealSize(request.getRealSize());

		int min = delayTime / 60;
		if(min == 0) {
			synchronized (writeTimerLock) {
				int nextSlot = secondsTimerWheel.countNextSlot(delayTime);
				TimerWheelSlot timerWheelSlot = secondsTimerWheel.getSlotList()[nextSlot];
				int maxOffset = timerLog.getMaxOffset();
				if(timerWheelSlot.getMsgCount() == 0) {
					timerLogUnit.setPrevPos(-1);
					timerWheelSlot.setFirstPos(maxOffset);
					timerWheelSlot.setLastPos(maxOffset);
				} else {
					timerLogUnit.setPrevPos(timerWheelSlot.getLastPos());
					timerWheelSlot.setLastPos(maxOffset);
				}
				timerWheelSlot.setMsgCount(timerWheelSlot.getMsgCount() + 1);
				timerLog.writeSlot(timerLogUnit);
				secondsTimerWheel.getSlotList()[nextSlot] = timerWheelSlot;
			}
		} else if(min > 0) {
			synchronized (writeTimerLock) {
				int nextSlot = minutesTimerWheel.countNextSlot(min);
				TimerWheelSlot timerWheelSlot = minutesTimerWheel.getSlotList()[nextSlot];
				int maxOffset = timerLog.getMaxOffset();
				if(timerWheelSlot.getMsgCount() == 0) {
					timerLogUnit.setPrevPos(-1);
					timerWheelSlot.setFirstPos(maxOffset);
					timerWheelSlot.setLastPos(maxOffset);
				} else {
					timerLogUnit.setPrevPos(timerWheelSlot.getLastPos());
					timerWheelSlot.setLastPos(maxOffset);
				}
				timerWheelSlot.setMsgCount(timerWheelSlot.getMsgCount() + 1);

				timerLog.writeSlot(timerLogUnit);
				minutesTimerWheel.getSlotList()[nextSlot] = timerWheelSlot;
			}
		}
	}

逻辑是,如果超过1分钟,则先放入分时间轮轮转,轮转出来后取模得出秒数,如果为0,则publish到对应的eventbus里执行逻辑,若不是,则放到秒时间轮中继续轮转。

需要注意的是为timerlogUnit设置prev,需要根据当前delay的timewheel的slot里存放的lastpos。这样迭代,我们可以先获取maxOffset作为最新的pos,然后直接设置prevPos,再写入timerLog。

然后是doMinutesScan和doSecondsScan,这里只放测试代码,后面还需要补上publish的操作。

	public void doMinutesScan() {
		synchronized (writeTimerLock) {
			int current = minutesTimerWheel.getCurrent();
			TimerWheelSlot minutesTimerWheelSlot = minutesTimerWheel.getSlotList()[current];
			TimerLogUnit[] timerLogUnits = timerLog.readList(minutesTimerWheelSlot);
			for (TimerLogUnit unit : timerLogUnits) {
				int remainSeconds = unit.getDelayedTime() % 60;
				if(remainSeconds > 0) {
					TimerRequest request = new TimerRequest();
					request.setDelayTime(remainSeconds);
					request.setRealOffset(unit.getRealOffset());
					request.setRealSize(unit.getRealSize());
					add(request);
				} else {
					// todo publish event
					logger.info("minutes wheel scan :{}", unit);
				}
			}

			// 执行清空逻辑
			minutesTimerWheel.getSlotList()[current] = new TimerWheelSlot();
			if(current == minutesTimerWheel.getSlotList().length - 1) {
				current = 0;
			} else {
				current = current + 1;
			}
			minutesTimerWheel.setCurrent(current);
		}
	}

	public void doSecondsScan() {
		synchronized (writeTimerLock) {
			int current = secondsTimerWheel.getCurrent();
			TimerWheelSlot secondsTimerWheelSlot = secondsTimerWheel.getSlotList()[current];
			TimerLogUnit[] timerLogUnits = timerLog.readList(secondsTimerWheelSlot);
			for(TimerLogUnit unit : timerLogUnits) {
				// todo publish event
				logger.info("seconds wheel scan :{}", unit);
			}
			// 执行完 清空
			secondsTimerWheel.getSlotList()[current] = new TimerWheelSlot();
			if(current == secondsTimerWheel.getSlotList().length - 1) {
				current = 0;
			} else {
				current = current + 1;
			}
			secondsTimerWheel.setCurrent(current);
		}
	}

分时间轮scan时需要注意,扫描出来后判断剩余delay,投放到秒时间轮,详细可以网上学一下分层时间轮算法。

2.3测试

测试代码

	public static void main(String[] args) {
		TimerWheelManager timerWheelManager = new TimerWheelManager();
		timerWheelManager.init(null);
		timerWheelManager.doScanWheel();
		TimerRequest tr1 = new TimerRequest();
		tr1.setDelayTime(62);
		tr1.setRealOffset(1);
		tr1.setRealSize(1);
		timerWheelManager.add(tr1);

		TimerRequest tr2 = new TimerRequest();
		tr2.setDelayTime(75);
		tr2.setRealOffset(1);
		tr2.setRealSize(1);
		timerWheelManager.add(tr2);

		TimerRequest tr3 = new TimerRequest();
		tr3.setDelayTime(75);
		tr3.setRealOffset(2);
		tr3.setRealSize(2);

		TimerRequest tr4 = new TimerRequest();
		tr4.setDelayTime(15);
		tr4.setRealOffset(3);
		tr4.setRealSize(3);
		timerWheelManager.add(tr3);
	}

测试结果

[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - current time is :14
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - current time is :15
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - current time is :16
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - seconds wheel scan :TimerLogUnit(realSize=4, realOffset=4, delayedTime=15, prevPos=575)
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - seconds wheel scan :TimerLogUnit(realSize=3, realOffset=3, delayedTime=15, prevPos=-1)
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - current time is :17
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - current time is :18
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - current time is :74
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - current time is :75
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - current time is :76
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - seconds wheel scan :TimerLogUnit(realSize=1, realOffset=1, delayedTime=15, prevPos=607)
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - seconds wheel scan :TimerLogUnit(realSize=2, realOffset=2, delayedTime=15, prevPos=-1)
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - current time is :77
[scan-timer-wheel-task] INFO com.prayer.broker.timerwheel.TimerWheelManager - current time is :78

image.png

3.延迟消息的实现

前面实现过了基于文件的timerwheel组件,接下来就是完善延迟消息的流程。大致思路为:在消息接收时判断是否为延迟消息,进入延迟消息处理流程,将延迟消息包装并发送到timer_topic中,同时构建一份TimerRequest,将TimerRequest投递到时间轮组件等待到期。当TimerRequest到期时,我们会从timerLog里取出之前包装的msg的offset和size帮助在timer_topic中寻找msg,拿出msg后直接投递到原本的topic下。

3.1延迟消息handler

public class PushMsgListener implements Listener<PushMsgEvent> {
	@Override
	public void onReceive(PushMsgEvent event) throws Exception {
		MessageDTO msg = event.getMessageDTO();
		boolean isDelayMsg = msg.getDelay() > 0;
		if (isDelayMsg) {
			appendDelayMessageV2(msg, event);
		}
	}
	
	public void appendDelayMessageV2(MessageDTO msg, PushMsgEvent event) throws IOException {
		String waitToPushTopic = "timer_topic";
		AssertUtil.isTrue(msg.getDelay() <= 3600, "to large delay time");
		MessageDTO delayMessage = new MessageDTO();
		delayMessage.setBody(JSON.toJSONBytes(msg));
		delayMessage.setTopic(waitToPushTopic);
		delayMessage.setQueueId(0);
		delayMessage.setSendWay(MsgSendWay.async.getValue());
		int[] re = BrokerContext.getCommitLogMMapFileModelManager().get(waitToPushTopic).writeTimerContent(delayMessage, true);
		int realOffset = re[0];
		int realSize = re[1];
		TimerRequest timerRequest = new TimerRequest();
		timerRequest.setRealSize(realSize);
		timerRequest.setRealOffset(realOffset);
		timerRequest.setDelayTime(msg.getDelay());
		BrokerContext.getTimerWheelManager().add(timerRequest);

		// 给client返回数据, 不用看
		sendResp();
	}
}
	

3.2 数据结构

appendDelayMessage的重点有二,一将消息投递至timer_topic队列,二是获取投递完的offset和size,不过这部分其实都是mq的mmap组件做的,不是本文的重点,因此不过多赘述。

获取到offset和size后便能构建TimerRequest,投递到时间轮中,时间轮在我上一篇文章有源码展示,其中有个eventBus,用于publish需要处理的数据,数据结构也很简单

TimerLogEvent event = new TimerLogEvent();
event.setTimerLogUnitList(timerLogUnitList);
eventBus.publish(event);

public class TimerLogEvent extends Event {
	private List<TimerLogUnit> timerLogUnitList;
}
public class TimerLogUnit {
	private int realSize;
	private int realOffset;
	private int delayedTime;
	private int prevPos;
}

3.3 延迟消息的恢复

进入到TimerLog的Listener之后,处理也很简单,找到timer_topic的mmapfile,取出之前存放的包装msg,然后将msg投放到origin的topic下。

public class TimerLogListener implements Listener<TimerLogEvent> {
	private final Logger logger = LoggerFactory.getLogger(this.getClass());
	@Override
	public void onReceive(TimerLogEvent event) throws Exception {
		List<TimerLogUnit> timerLogUnitList = event.getTimerLogUnitList();
		for (TimerLogUnit timerLogUnit : timerLogUnitList) {
			int realOffset = timerLogUnit.getRealOffset();
			int realSize = timerLogUnit.getRealSize();
			CommitLogMMapFileModel commitLogMMapFileModel = BrokerContext.getCommitLogMMapFileModelManager().get("timer_topic");
			ConsumeMsgCommitLogDTO consumeMsgCommitLogDTO = commitLogMMapFileModel.readContent(realOffset, realSize);
			byte[] body = consumeMsgCommitLogDTO.getBody();
			MessageDTO originMessage = JSON.parseObject(body, MessageDTO.class);
			logger.info("TimerLog msg={}", JSON.toJSONString(originMessage));
			BrokerContext.getCommitLogAppendHandler().appendMsg(originMessage);
		}
	}
}

3.4测试

测试代码

	@Test Producer
	public void sendUserEnterMsg() throws InterruptedException {
		for (int i = 80; i < 90; i++) {
			try {
				MessageDTO messageDTO = new MessageDTO();
				messageDTO.setTopic("order_cancel_topic");
				JSONObject jsonObject = new JSONObject();
				jsonObject.put("orderId",i);
				jsonObject.put("level",1);
				jsonObject.put("cancelTime",System.currentTimeMillis());
				messageDTO.setBody(jsonObject.toJSONString().getBytes());
				messageDTO.setDelay(5);
				SendResult sendResult = producer.send(messageDTO);
				System.out.println(JSON.toJSONString(sendResult));
			}catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	@Test Consumer
	public void testConsumeMsg() throws InterruptedException{
		consumer = new DefaultConsumer();
		consumer.setNsIp("127.0.0.1");
		consumer.setNsPort(9090);
		consumer.setTopic("order_cancel_topic");
		consumer.setConsumeGroup("test-order_cancel_topic-group");
		consumer.setBrokerClusterGroup("test-group");
		consumer.setBatchSize(1);
		consumer.setMessageConsumeListener(new MessageConsumeListener() {
			@Override
			public ConsumeResult consume(List<ConsumeMessage> consumeMessageList) {
				for(ConsumeMessage consumeMessage : consumeMessageList){
					System.out.println(System.currentTimeMillis() + " " + new String(consumeMessage.getConsumeMsgCommitLogDTO().getBody()));
				}
				return ConsumeResult.consume_success();
			}
		});
		consumer.start();
	}

启动nameserver,broker-master,broker-slave,consumer

消费结果如下:

image.png 5s数据

1739164707790 {"orderId":86,"level":1,"cancelTime":1739164701453}
[consume_msg_task] INFO com.prayer.client.consumer.DefaultConsumer - 消费成功, topic=order_cancel_topic
1739164707811 {"orderId":89,"level":1,"cancelTime":1739164701459}
[consume_msg_task] INFO com.prayer.client.consumer.DefaultConsumer - 消费成功, topic=order_cancel_topic
1739164707812 {"orderId":82,"level":1,"cancelTime":1739164701445}
[consume_msg_task] INFO com.prayer.client.consumer.DefaultConsumer - 消费成功, topic=order_cancel_topic
1739164707926 {"orderId":85,"level":1,"cancelTime":1739164701451}
[consume_msg_task] INFO com.prayer.client.consumer.DefaultConsumer - 消费成功, topic=order_cancel_topic
1739164707927 {"orderId":88,"level":1,"cancelTime":1739164701457}
[consume_msg_task] INFO com.prayer.client.consumer.DefaultConsumer - 消费成功, topic=order_cancel_topic
1739164708036 {"orderId":84,"level":1,"cancelTime":1739164701449}
[consume_msg_task] INFO com.prayer.client.consumer.DefaultConsumer - 消费成功, topic=order_cancel_topic
1739164708037 {"orderId":87,"level":1,"cancelTime":1739164701455}
[consume_msg_task] INFO com.prayer.client.consumer.DefaultConsumer - 消费成功, topic=order_cancel_topic
1739164708146 {"orderId":81,"level":1,"cancelTime":1739164701443}
[consume_msg_task] INFO com.prayer.client.consumer.DefaultConsumer - 消费成功, topic=order_cancel_topic
1739164708147 {"orderId":83,"level":1,"cancelTime":1739164701447}
[consume_msg_task] INFO com.prayer.client.consumer.DefaultConsumer - 消费成功, topic=order_cancel_topic
1739164708256 {"orderId":80,"level":1,"cancelTime":1739164701382}
[consume_msg_task] INFO com.prayer.client.consumer.DefaultConsumer - 消费成功, topic=order_cancel_topic

前面的是消费的currentTimeStamp,消息体中的是发送时的currentTimerStamp,延迟差不多是5s这样。至此就初步实现了一套延迟消息机制。


4.todo

  • timerlog文件可能会被写满,需要进行轮转,轮转逻辑的设计
  • timerslot掉电恢复
  • timerlog的IO操作优化