概述

在上文中,我们讨论了消费者对于消息拉取的实现,对于 RocketMQ 这个黑盒的心脏部分,我们顺着消息的发送流程已经将其剖析了大半部分。本章我们不妨乘胜追击,接着讨论各种不同的消息的原理与实现。

事务消息

概念

RocketMQ 中的事务消息功能,实际上是 分布式事务中的本地事务表 的实现,只不过,在这里用消息中间件来代替了数据库,同时也帮我们做好了回查的操作。

在这点上,RocketMQ 和 Kafka 是截然不同的,kafka 的事务是用来实现 Exacltly Once 语义,且该语义主要用来流计算中,即在 "从 Topic 中读 -> 计算 -> 存到 Topic" 保证不被重复计算。

事务流程
  1. 客户端发送 half 消息

吐槽一下为什么要叫半消息(half message),叫 prepare 消息不是更直观吗

  1. Broker 将 half 消息持久化
  2. 客户端根据事务执行结果,发送 Commit / Rollback 消息
  3. Broker 收到 Commit 时,将事务消息对消费者可见。收到 Rollback 时,将消息丢弃
补偿
  1. Broker 过久未收到事务执行结果,询问客户端执行结果
  2. 客户端收到结果查询请求,执行回查方法,发送 Commit / Rollback 方法
  3. Broker 根据事务执行结果做出对应处理

源码流程

第一步

在设置好了事务监听器后(执行事务 与 事务回查),就可以发送事务消息

在将事务消息交给发送方法后,客户端首先会为消息添加事务消息的标识

  1. MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");

然后将该事务消息会像普通的同步消息一样发送(且是同步发送)

  1. sendResult = this.send(msg);

具体发送流程见:RocketMQ源码详解 | Producer篇 · 其一:Start,然后 Send 一条消息

第二步

在 Broker 端接收到消息以后,会走与普通消息相同的底层通道(因为这个消息本身就只是个加上了 事务flag 的普通消息),然后由 TransactionalMessageService 来对这个消息进行额外处理。

首先会对该消息放入 real topic 属性和 real queue 属性,然后将消息 Topic 替换为用于处理所有事务消息的特殊的 Topic,当然该 Topic 对消费者是不可见的。

  1. private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
  2. MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
  3. MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
  4. String.valueOf(msgInner.getQueueId()));
  5. // 设置标记为未收到结果
  6. msgInner.setSysFlag(
  7. MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));
  8. // 替换到特殊的 Topic (RMQ_SYS_TRANS_HALF_TOPIC)
  9. msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
  10. msgInner.setQueueId(0);
  11. msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
  12. return msgInner;
  13. }

完成后,会送到 MessageStore 像普通消息一样处理

普通消息的具体流程见 RocketMQ源码详解 | Broker篇 · 其二:文件系统

第三步

回到 Producer 端,在事务消息发送完成后,该方法会使用专门的线程池执行事务

  1. // 2.执行本地事务,更新事务获取状态
  2. localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);

然后对本地的事务执行状态进行处理,也就是将该执行状态上报

  1. this.endTransaction(msg, sendResult, localTransactionState, localException);

这里会发送一条 oneway 命令给 Broker 端,且使用的是 RequestCode.END_TRANSACTION 请求码

  1. // 事务结果报告(可能是 commit 或 rollback)
  2. public static final int END_TRANSACTION = 37;

完成处理后,该方法会将事务的发送结果和本地事务的执行结构都返回给上层 API

第四步

在 Broker 端,这里会由 EndTransactionProcessor 处理器来处理该请求码

然后,根据事务的执行结果来做不同的处理

  1. if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
  2. // 事务执行成功,尝试完成事务
  3. // 获取 half 消息
  4. result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
  5. if (result.getResponseCode() == ResponseCode.SUCCESS) {
  6. if (res.getCode() == ResponseCode.SUCCESS) {
  7. // 将 half 消息取出,构造真实消息,然后投入实际上的 Topic
  8. /* pass */
  9. RemotingCommand sendResult = sendFinalMessage(msgInner);
  10. if (sendResult.getCode() == ResponseCode.SUCCESS) {
  11. /*
  12. * 找到半消息,进行删除
  13. * 删除并不是物理上的删除,因为物理上的删除的代价十分的高昂,而是写入一条具有相同事务id的消息到 op Topic
  14. */
  15. this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
  16. }
  17. return sendResult;
  18. }
  19. return res;
  20. }
  21. }

如果需要回滚,则对相应的半消息进行删除,且和上面一样,并不是物理上的删除,而是发送具有相同事务 id 的消息到 OP Topic,来标记这个事务已经完成了(Commit/Rollback), OP Topic 也是一个特殊的 Topic,同样对消费者不可见。

  1. if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
  2. // 事务执行失败,进行 half 消息的回滚
  3. // 首先找到 half 消息
  4. result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);
  5. if (result.getResponseCode() == ResponseCode.SUCCESS) {
  6. RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
  7. if (res.getCode() == ResponseCode.SUCCESS) {
  8. // 进行删除
  9. this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
  10. }
  11. return res;
  12. }
  13. }

当这些都做完后,一次事务就完成了。

补偿

当然啦,以上是顺利的情况,我们当然不能指望事务每一次都能执行成功、网络分区和宕机事件永远不会发生。

在一段时间后,如果客户端没有对事务的状态进行上报(或者上报的状态不是 Commit 或 Rollback,而是 Unknown), Broker 端当然就要进行事务状态的回查。

BrokerController 启动的时候,会开启事务状态检测服务,该服务会通过循环调用 TransactionalMessageServiceImpl.check() 方法,不断的扫描未结束的事务,同时对超过指定时间还不知道状态的事务进行回查操作。

check() 方法是事务回查的核心,由于很长,我们先来看第一部分(删减了没人在意的 Log)

  1. // 首先找到存储所有 half 消息的 Topic
  2. String topic = TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC;
  3. Set<MessageQueue> msgQueues = transactionalMessageBridge.fetchMessageQueues(topic);
  4. // 对其中每一个 queue 进行检查
  5. for (MessageQueue messageQueue : msgQueues) {
  6. long startTime = System.currentTimeMillis();
  7. // 获得对应的 op 消息所在的 queue
  8. MessageQueue opQueue = getOpQueue(messageQueue);
  9. // 获取未处理的 half 消息的起始偏移量
  10. long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue);
  11. // 获取 op 消息的 queue 的起始偏移量
  12. long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue);
  13. // 用来记录已经被处理了的 op 消息的偏移量
  14. List<Long> doneOpOffset = new ArrayList<>();
  15. // 用来记录已经完成了的 half 消息的偏移量
  16. // key: halfOffset, value: opOffset
  17. HashMap<Long, Long> removeMap = new HashMap<>();
  18. PullResult pullResult = fillOpRemoveMap(removeMap, opQueue, opOffset, halfOffset, doneOpOffset);

fillOpRemoveMap 方法中,主要是将 op 消息取出,来标记可以被移除的 half 消息(op 消息的存在代表对应事务的结束

  1. /**
  2. * 读取op消息,解析op消息,填充removeMap
  3. *
  4. * @param removeMap 要删除的半消息,key: halfOffset,value: opOffset
  5. * @param opQueue Op message queue.
  6. * @param pullOffsetOfOp op message queue 的起始偏移量
  7. * @param miniOffset half message queue 的当前最小偏移量
  8. * @param doneOpOffset 存储已处理的 op 消息
  9. * @return 获取到的 Op 消息
  10. */
  11. private PullResult fillOpRemoveMap(HashMap<Long, Long> removeMap,
  12. MessageQueue opQueue, long pullOffsetOfOp, long miniOffset, List<Long> doneOpOffset) {
  13. // 首先通过 queue 获取 op 消息,最大数量为 32 条
  14. PullResult pullResult = pullOpMsg(opQueue, pullOffsetOfOp, 32);
  15. /* pass: pullResult 消息的意外状态的处理 */
  16. List<MessageExt> opMsg = pullResult.getMsgFoundList();
  17. for (MessageExt opMessageExt : opMsg) {
  18. // op 消息的 body 存储的是对应的 half 消息的偏移量, 现在将其取出
  19. Long queueOffset = getLong(new String(opMessageExt.getBody(), TransactionalMessageUtil.charset));
  20. // 感觉这里的 Tag 并没有什么意义,无论是 Commit 还是 Rollback 都会加入这个 Tag
  21. if (TransactionalMessageUtil.REMOVETAG.equals(opMessageExt.getTags())) {
  22. // 在 已处理偏移量 之前的话则可直接放入 已处理偏移量集合
  23. if (queueOffset < miniOffset) {
  24. doneOpOffset.add(opMessageExt.getQueueOffset());
  25. } else {
  26. // 否则放入需要移除的 half 的消息的集合
  27. removeMap.put(queueOffset, opMessageExt.getQueueOffset());
  28. }
  29. }
  30. }
  31. return pullResult;
  32. }

然后进入到 check 方法的第二部分

  1. while (true) {
  2. if (System.currentTimeMillis() - startTime > MAX_PROCESS_TIME_LIMIT) break;
  3. // 推进最小已处理偏移量
  4. if (removeMap.containsKey(i)) /* 如果该 half 消息存在对应的 op 消息,说明已经被处理了(commit/rollback) */ {
  5. // 取出放入到已处理偏移量队列
  6. Long removedOpOffset = removeMap.remove(i);
  7. doneOpOffset.add(removedOpOffset);
  8. } else /* 否则说明当前 half 消息悬而未决 */ {
  9. // 取出对应的半消息
  10. GetResult getResult = getHalfMsg(messageQueue, i);
  11. /* pass: 半消息不存在时的意外处理 */
  12. /*
  13. * 检测是否要丢弃或跳过
  14. * 丢弃条件: 当前事务已经超过了最大回查次数(15次)
  15. * 跳过条件: 已经超过了过期文件最大保留时间(72小时)
  16. */
  17. if (needDiscard(msgExt, transactionCheckMax) || needSkip(msgExt)) {
  18. // 处理并推进偏移量
  19. // 具体的处理方法是: 投入 TRANS_CHECK_MAX_TIME_TOPIC 这个 Topic,等待手动处理
  20. listener.resolveDiscardMsg(msgExt);
  21. // 进入到下一个 half 消息
  22. newOffset = i + 1;
  23. i++;
  24. continue;
  25. }
  26. if (msgExt.getStoreTimestamp() >= startTime) {
  27. break;
  28. }

上面的方法很好理解,只是对于已经被标记结束的事务的处理、和未结束事务的补足

接下来是第三部分,这里将继续对未结束事务的补足,与进行可能的回查操作

  1. // half 消息具有最小的检查时间(免疫时间), 检测时间以内可以跳过回查, 重新投入 half 消息的 Topic
  2. long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp();
  3. long checkImmunityTime = transactionTimeout;
  4. String checkImmunityTimeStr = msgExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS);
  5. if (null != checkImmunityTimeStr) {
  6. checkImmunityTime = getImmunityTime(checkImmunityTimeStr, transactionTimeout);
  7. if (valueOfCurrentMinusBorn < checkImmunityTime) {
  8. if (checkPrepareQueueOffset(removeMap, doneOpOffset, msgExt)) {
  9. newOffset = i + 1;
  10. i++;
  11. continue;
  12. }
  13. }
  14. } else {
  15. if ((0 <= valueOfCurrentMinusBorn) && (valueOfCurrentMinusBorn < checkImmunityTime)) {
  16. break;
  17. }
  18. }
  19. /*
  20. * 对于当前事务的回查操作,需要满足三个条件之一
  21. * 1.当前 op 消息的集合为空,且已经超过了最小检查时间(免疫时间)
  22. * 2.最大偏移量的 op 消息的生成时间 已经超过了 最小检查时间
  23. * 3.关闭最小检查时间
  24. */
  25. List<MessageExt> opMsg = pullResult.getMsgFoundList();
  26. boolean isNeedCheck = (opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime)
  27. || (opMsg != null && (opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout))
  28. || (valueOfCurrentMinusBorn <= -1);
  29. if (isNeedCheck) {
  30. // 先将当前 half 消息放回
  31. if (!putBackHalfMsgQueue(msgExt, i)) {
  32. continue;
  33. }
  34. // 然后向 Product 发送检测消息
  35. listener.resolveHalfMsg(msgExt);
  36. } else {
  37. // 否则更新 op 消息集合,以确保能够断言该 half 消息的状态
  38. pullResult = fillOpRemoveMap(removeMap, opQueue, pullResult.getNextBeginOffset(), halfOffset, doneOpOffset);
  39. continue;
  40. }
  41. }
  42. newOffset = i + 1;
  43. i++;
  44. }

上面这段代码主要围绕 "是否进行回查" 展开,且涉及到 "免疫时间"。

在一个事务消息被发送后,对应事务的执行当然需要一定的执行时间,如果我们不设置这个时间立刻进行回查,那么很有可能时候事务还没执行完,对于大多数情况下还没执行完的事务进行回查,毫无疑问带来的收益很低。所以我们需要设定一个时间,在这个时间内的事务先暂时不回查,这个时间就叫做"免疫时间"。

然后再来看下需要进行回查的三种情况:

  1. 当 op 消息的集合为空,说明当前还没有收到让当前事务结束的通知,且超过了"免疫时间",故回查
  2. 当前 op 消息最大偏移量的生成时间超过了"免疫时间",说明该事务的提交消息可能丢失了,故回查
  3. 不启用 "免疫时间"

其中发送的回查消息的请求码为 RequestCode.CHECK_TRANSACTION_STATE ,发送的也是 oneway 消息

最后的第四部分,同时更新 half 和 op 消息在 Queue 中的偏移量

  1. // 对所有的 half 消息计算完成后,更新偏移量
  2. if (newOffset != halfOffset) {
  3. transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset);
  4. }
  5. // 根据已经被标记为完成的 op 消息更新偏移量
  6. long newOpOffset = calculateOpOffset(doneOpOffset, opOffset);
  7. if (newOpOffset != opOffset) {
  8. // 如果不等,说明并不是所有的 op 消息都被标记为完成了
  9. // 所以我们只将偏移量更新到第一个未完成的 op 消息的位置,其后面的 op 消息会在下次重复处理
  10. transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset);
  11. }

然后在 Producer 这边,将由 ClientRemotingProcessor.checkTransactionState() 来处理回查操作

  1. // 获取事务 ID
  2. String transactionId = messageExt.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
  3. if (null != transactionId && !"".equals(transactionId)) {
  4. messageExt.setTransactionId(transactionId);
  5. }
  6. final String group = messageExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP);
  7. if (group != null) {
  8. // 从 MQClientFactory 找到注册的对应 Producer
  9. MQProducerInner producer = this.mqClientFactory.selectProducer(group);
  10. if (producer != null) {
  11. final String addr = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
  12. // 让 Producer 检查在对应 IP 上的事务状态
  13. producer.checkTransactionState(addr, messageExt, requestHeader);
  14. } else {
  15. log.debug("checkTransactionState, pick producer by group[{}] failed", group);
  16. }
  17. } else {
  18. log.warn("checkTransactionState, pick producer group failed");
  19. }

再进入 producer.checkTransactionState() 看看 Producer 是怎样检查事务状态的

  1. TransactionCheckListener transactionCheckListener = DefaultMQProducerImpl.this.checkListener();
  2. // 取出当前 Producer 的事务监听器
  3. TransactionListener transactionListener = getCheckListener();
  4. if (transactionCheckListener != null || transactionListener != null) {
  5. LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
  6. Throwable exception = null;
  7. try {
  8. if (transactionCheckListener != null) {
  9. // 调用其的事务回查方法
  10. localTransactionState = transactionCheckListener.checkLocalTransactionState(message);
  11. } else if (transactionListener != null) {
  12. log.debug("Used new check API in transaction message");
  13. localTransactionState = transactionListener.checkLocalTransaction(message);
  14. }
  15. } catch (Throwable e) {
  16. log.error("Broker call checkTransactionState, but checkLocalTransactionState exception", e);
  17. exception = e;
  18. }
  19. // 再将事务执行结果其发回给 Broker
  20. this.processTransactionState(
  21. localTransactionState,
  22. group,
  23. exception);
  24. } else {
  25. log.warn("CheckTransactionState, pick transactionCheckListener by group[{}] failed", group);
  26. }

最后发回的方法做的事情和在一开始发送事务状态的方法,所做的事情是一样的。Broker 做的处理也是一样的。

这样,补偿流程就执行完了。

批量消息

概念

在消息队列中,批量消息也是一个重要的部分,将消息压缩在一起发送不仅可以减少带宽的消耗,还能节省头部占用的空间。

有点失望的是,RocketMQ 对于批量消息的实现有点"粗糙"了

源码流程

首先,在调用 send() 的 batch 版本后,会先对批量消息进行校验

批量消息不允许延时、不允许发送到重试 Topic,且要求发送到的 Topic 必须是同一个 Topic

  1. List<Message> messageList = new ArrayList<Message>(messages.size());
  2. Message first = null;
  3. for (Message message : messages) {
  4. if (message.getDelayTimeLevel() > 0) {
  5. throw new UnsupportedOperationException("TimeDelayLevel is not supported for batching");
  6. }
  7. if (message.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
  8. throw new UnsupportedOperationException("Retry Group is not supported for batching");
  9. }
  10. if (first == null) {
  11. first = message;
  12. } else {
  13. if (!first.getTopic().equals(message.getTopic())) {
  14. throw new UnsupportedOperationException("The topic of the messages in one batch should be the same");
  15. }
  16. if (first.isWaitStoreMsgOK() != message.isWaitStoreMsgOK()) {
  17. throw new UnsupportedOperationException("The waitStoreMsgOK of the messages in one batch should the same");
  18. }
  19. }
  20. messageList.add(message);
  21. }
  22. MessageBatch messageBatch = new MessageBatch(messageList);

在校验完成,且都放到一个 List 之后,接下来的步骤和普通的消息发送都差不多,只是在编码上理所当然的存在着不同

  1. public static byte[] encodeMessages(List<Message> messages) {
  2. //TO DO refactor, accumulate in one buffer, avoid copies
  3. List<byte[]> encodedMessages = new ArrayList<byte[]>(messages.size());
  4. int allSize = 0;
  5. for (Message message : messages) {
  6. // 编码每一个消息
  7. byte[] tmp = encodeMessage(message);
  8. encodedMessages.add(tmp);
  9. allSize += tmp.length;
  10. }
  11. // 放到最后的大集合中
  12. byte[] allBytes = new byte[allSize];
  13. int pos = 0;
  14. for (byte[] bytes : encodedMessages) {
  15. System.arraycopy(bytes, 0, allBytes, pos, bytes.length);
  16. pos += bytes.length;
  17. }
  18. return allBytes;
  19. }

然后使用 RequestCode.SEND_BATCH_MESSAGE 这个状态码发送出去。

在 Broker 端,其投入的过程大体上和普通消息类似,但是其最后的持久化到硬盘时,这块批量消息被拆分为了普通的单条消息。

即 RocketMQ 使用批量消息只减少了发送时的宽带传输,对于存储与交给消费者的部分并没有获得优化

  1. // 拆分批量消息为每一个普通消息
  2. while (messagesByteBuff.hasRemaining()) {
  3. // 1 TOTALSIZE
  4. final int msgPos = messagesByteBuff.position();
  5. final int msgLen = messagesByteBuff.getInt();
  6. final int bodyLen = msgLen - 40; //only for log, just estimate it
  7. /* pass: 当作普通消息存储 */
  8. queueOffset++;
  9. msgNum++;
  10. messagesByteBuff.position(msgPos + msgLen);
  11. }

延时消息

概念

在业务中,有时候有一些延时提交任务的需求,这时候就可以使用延时消息,即在投递一部分时间后才对消费者可见。

不过,在 RocketMQ 中,延迟级别并不支持自定义,而是具有固定的延迟级别。

不过商业版的 阿里云MQ 可以支持秒精度的自定义延迟时间,果然是为了阉割社区版来赚钱吗

源码流程

RocketMQ 对于延时消息的处理主要在于 Broker 端,所以我们只需要看在 Broker 对延时级别的处理。

首先,在 CommitLog 的 put 中,会对延迟级别进行判断,如果存在,会在这进行进行 Topic 的替换,将其存储到对应的延迟级别的 Queue

  1. if (msg.getDelayTimeLevel() > 0) {
  2. if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
  3. msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
  4. }
  5. topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
  6. queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
  7. // Backup real topic, queueId
  8. MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
  9. MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
  10. msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
  11. msg.setTopic(topic);
  12. msg.setQueueId(queueId);
  13. }

然后会被在 DefaultMessageStore 中初始化的 ScheduleMessageService 处理

首先,该服务在启动时会进行初始化

  1. public void start() {
  2. // 保证只被执行一次
  3. if (started.compareAndSet(false, true)) {
  4. // 加载本地快照
  5. super.load();
  6. this.timer = new Timer("ScheduleMessageTimerThread", true);
  7. for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
  8. // 取出每一个级别
  9. Integer level = entry.getKey();
  10. // 当前延迟级别对应的延迟时间
  11. Long timeDelay = entry.getValue();
  12. // 该延迟级别之前消费到的自己的队列的偏移量
  13. Long offset = this.offsetTable.get(level);
  14. if (null == offset) {
  15. offset = 0L;
  16. }
  17. // 每一个延迟级别设置一个定时任务
  18. if (timeDelay != null) {
  19. this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
  20. }
  21. }
  22. // 定时持久化各个延迟级别的偏移量
  23. this.timer.scheduleAtFixedRate(new TimerTask() {
  24. @Override
  25. public void run() {
  26. try {
  27. if (started.get()) ScheduleMessageService.this.persist();
  28. } catch (Throwable e) {
  29. log.error("scheduleAtFixedRate flush exception", e);
  30. }
  31. }
  32. }, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
  33. }
  34. }

每一个延迟级别的 Queue 都有对应的定时任务,且都会执行以下方法

  1. public void executeOnTimeup() {
  2. // 找到自己延迟级别的消费队列
  3. ConsumeQueue cq =
  4. ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC,
  5. delayLevel2QueueId(delayLevel));
  6. long failScheduleOffset = offset;
  7. if (cq != null) {
  8. // 根据消费偏移量将指定的 MappedFile 文件加载进来
  9. SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
  10. if (bufferCQ != null) {
  11. try {
  12. long nextOffset = offset;
  13. int i = 0;
  14. // 遍历每一个消息的索引
  15. ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
  16. for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
  17. long offsetPy = bufferCQ.getByteBuffer().getLong();
  18. int sizePy = bufferCQ.getByteBuffer().getInt();
  19. long tagsCode = bufferCQ.getByteBuffer().getLong();
  20. /* pass */
  21. long now = System.currentTimeMillis();
  22. long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);
  23. nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
  24. long countdown = deliverTimestamp - now;
  25. if (countdown <= 0) /* 目标时间小于当起时间,可以执行 */ {
  26. // 根据偏移量取出消息
  27. MessageExt msgExt =
  28. ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(
  29. offsetPy, sizePy);
  30. if (msgExt != null) {
  31. try {
  32. // 将延迟消息恢复成原本消息的样子
  33. MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);
  34. /* pass */
  35. // 投入真实的 Topic
  36. PutMessageResult putMessageResult =
  37. ScheduleMessageService.this.writeMessageStore
  38. .putMessage(msgInner);
  39. /* pass: 更新度量信息 */
  40. } catch (Exception e) {
  41. /* pass */
  42. }
  43. }
  44. } else /* 否则,这个消息需要被消费的时间到了再通知我 */ {
  45. ScheduleMessageService.this.timer.schedule(
  46. new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset),
  47. countdown);
  48. // 更新消费偏移量
  49. ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
  50. return;
  51. }
  52. } // end of for
  53. // 走到这里,说明暂时没有需要消费的延时消息
  54. nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
  55. // 小睡一会
  56. ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
  57. this.delayLevel, nextOffset), DELAY_FOR_A_WHILE);
  58. ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
  59. return;
  60. } finally {
  61. bufferCQ.release();
  62. }
  63. } // end of if (bufferCQ != null)
  64. /* pass */
  65. } // end of if (cq != null)
  66. ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel,
  67. failScheduleOffset), DELAY_FOR_A_WHILE);
  68. }

可以看出,延迟消息的实现还是十分简单的,由于先投入的延时消息必先快于后投入的消息的到期,所以只需要不断的拉取各个延迟级别对应的队列 的头部的延迟消息即可。这也是只支持固定级别的延迟消息带来的好处。

RocketMQ源码详解 | Broker篇 · 其四:事务消息、批量消息、延迟消息的更多相关文章

  1. RocketMQ源码详解 | Broker篇 · 其三:CommitLog、索引、消费队列

    概述 上一章中,已经介绍了 Broker 的文件系统的各个层次与部分细节,本章将继续了解在逻辑存储层的三个文件 CommitLog.IndexFile.ConsumerQueue 的一些细节.文章最后 ...

  2. RocketMQ源码详解 | Broker篇 · 其一:线程模型与接收链路

    概述 在上一节 RocketMQ源码详解 | Producer篇 · 其二:消息组成.发送链路 中,我们终于将消息发送出了 Producer,在短暂的 tcp 握手后,很快它就会进入目的 Broker ...

  3. RocketMQ源码详解 | Broker篇 · 其五:高可用之主从架构

    概述 对于一个消息中间件来讲,高可用功能是极其重要的,RocketMQ 当然也具有其对应的高可用方案. 在 RocketMQ 中,有主从架构和 Dledger 两种高可用方案: 第一种通过主 Brok ...

  4. RocketMQ源码详解 | Broker篇 · 其二:文件系统

    概述 在 Broker 的通用请求处理器将一个消息进行分发后,就来到了 Broker 的专门处理消息存储的业务处理器部分.本篇文章,我们将要探讨关于 RocketMQ 高效的原因之一:文件结构的良好设 ...

  5. RocketMQ源码详解 | Consumer篇 · 其一:消息的 Pull 和 Push

    概述 当消息被存储后,消费者就会将其消费. 这句话简要的概述了一条消息的最总去向,也引出了本文将讨论的问题: 消息什么时候才对被消费者可见? 是在 page cache 中吗?还是在落盘后?还是像 K ...

  6. RocketMQ源码详解 | Producer篇 · 其二:消息组成、发送链路

    概述 在上一节 RocketMQ源码详解 | Producer篇 · 其一:Start,然后 Send 一条消息 中,我们了解了 Producer 在发送消息的流程.这次我们再来具体下看消息的构成与其 ...

  7. RocketMQ源码详解 | Producer篇 · 其一:Start,然后 Send 一条消息

    概述 DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); ...

  8. Linux内核源码详解——命令篇之iostat[zz]

    本文主要分析了Linux的iostat命令的源码,iostat的主要功能见博客:性能测试进阶指南——基础篇之磁盘IO iostat源码共563行,应该算是Linux系统命令代码比较少的了.源代码中主要 ...

  9. [转]Linux内核源码详解--iostat

    Linux内核源码详解——命令篇之iostat 转自:http://www.cnblogs.com/york-hust/p/4846497.html 本文主要分析了Linux的iostat命令的源码, ...

随机推荐

  1. win32 TreeCtrl控件通知消息, LVN_SELCHANGED和LVN_ITEMCHANGED用法

    今天出了个奇怪的问题,当我在主窗口上创建一个用模板对话框的子窗口时, 在子窗口上放的TreeCtrl控件不响应LVN_SELCHANGED消息,也是晕死了, 我以为是消息捕获的问题,我在主窗口上也捕获 ...

  2. Vue组件间的数据传输

    1.父组件向子组件传输数据:自定义属性 1 //父组件 2 <Son :msg="message" :user="userinfo"></So ...

  3. SpaCy下载及安装

    SpaCy可以说是坑多到怀疑人生.. 但是看在它那么功能那么强大的份上,我还是决定原谅它哈哈哈~ 1.首先用官网给的命令快速安装纯属扯淡..(结果就是一直拒绝你的连接) 官网:https://spac ...

  4. C++面向行输入:get()与getline()

    面向行的输入:get()与getline() 引入: char a = 's';//这样的语句合法 char b = "s";//不合法 /* "S"不是字符常 ...

  5. 北鲲云超算如何让仿真技术、HPC和人工智能之间的深度融合?

    在CAE领域,随着仿真技术在多个行业的深度应用,也带来了仿真模型日益复杂.仿真过程数据倍增.仿真计算费用昂贵等问题,降阶模型.人工智能.云计算等多种技术和仿真技术的深度融合,成为了仿真技术的重要发展趋 ...

  6. Shell 编程 基础用法

    Shell 编程 更改shell脚本权限 chmod u+x shell.sh 标准头部写法 #! /bin/bash #! /bin/dash 变量使用 a=10 print $a 读取命令行参数 ...

  7. 图解java 多线程模式 读书笔记

    第1章"Single Threaded Execution模式--能通过这座桥的只有一个人" 该模式可以确保执行处理的线程只能是一个,这样就可以有效防止实例不一致. 第⒉章&quo ...

  8. C/C++入门级小游戏——开发备忘录

    很多工科的学生在大一都有一门课程,叫C语言程序设计.大概就是装个IDE然后和一个黑乎乎的窗口打交道,期末到了考完试就结束了.然而很多人可能都有一个疑惑:C语言究竟能干什么?除开嵌入式单片机这些高大上的 ...

  9. K8s容器存储接口(CSI)介绍

    Container Storage Interface是由来自Kubernetes.Mesos.Docker等社区member联合制定的一个行业标准接口规范,旨在将任意存储系统暴露给容器化应用程序. ...

  10. 2020BUAA软工个人博客作业

    2020BUAA软工个人博客作业 17373010 杜博玮 项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 个人博客作业 我在这个课程的目标是 学 ...