参考代码:
rocketmq-release-5.1.1
参考文章:
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是怎么处理定时消息的。
涉及到的线程
- TimerEnqueueGetService
- TimerEnqueuePutService
- TimerDequeueGetService
- TimerDequeueGetMessageService
- TimerDequeuePutMessageService
涉及到的队列
- enqueuePutQueue
- dequeueGetQueue
- dequeuePutQueue
围绕这几个线程和queue来看,rocketmq的时间轮就非常清晰。
流程如下:
-
将普通消息转换为wheel_time消息,写入timer_topic,在PutMessage前处理
- 统一时间戳,如果还是设置的level则走老流程
- 转换成wheel_time消息,将原本消息的topic,queueid存储为properties,将topic更换为timer_topic,返回
-
开启一个TimerEnqueueGetService线程,将timer_topic中的msg读取并构成TimerRequest,投递到enqueuePutQueue中
-
开启一个TimerEnqueuePutService线程,处理TimerRequest,将消息放入时间轮或直接进入dequeuePutQueue
-
时间轮enqueue逻辑(加入时间轮)
- 处理滚动逻辑(这部分还没完全看懂),根据延迟时间定位到对应的slot,将消息顺序写入timerlog
- 更新TimerWheel中的slot信息,将指向timerlog的指针对象,包括first_pos和last_pos以及delayed_time等写入。
-
时间轮dequeue逻辑(消费时间轮)
- 开启TimerDequeueGetService,根据时间找到时间轮中的slot
- 根据slot存储的指针,读取timerlog中所有的timerlog单位组装成TimerRequest,将其放入dequeueGetQueue
- 开启TimerDequeueGetMessageService从commitlog中读取消息,写入TimerRequest,提交到dequeuePutQueue
- 开启TimerDequeuePutMessageService将dequeuePutQueue的消息转换并投递到真实的topic
- 定时/延迟消息的HA机制
实现思路-整体流程图
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
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
消费结果如下:
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操作优化