RocketMQ4.9.1源码分析_(事务) Half半消息处理

1,892 阅读3分钟

前言

half消息属于RocketMQ事务处理的第一个阶段,可以包括两部分:

  1. producer -> broker 发送half消息
  2. broker 处理half消息

对应图中的步骤1、2。

带着问题看源码, 先提几个问题:

  1. producer 如何发送 half 消息的?
  2. broker 又是如何区分普通消息和事务消息的
  1. half消息不会被Consumer消费,是如何实现的?

看完就有答案了。

Producer Half 消息发送

首先生产者的使用代码如下,要使用事务消息的功能,生产者对象要使用TransactionMQProducer进行声明。

public static void main(String[] args) throws MQClientException {
    // 创建 TransactionMQProducer 实例,并设置生产者组名
    TransactionMQProducer producer = new TransactionMQProducer("transactionGroup");
    // 设置 NameServer 地址
    producer.setNamesrvAddr("127.0.0.1:9876");

    // 添加事务监听器
    producer.setTransactionListener(new TransactionListener() {
        /**
         * 执行本地事务的方法
         */
        @Override
        public LocalTransactionState executeLocalTransaction(Message message, Object o) {
            // 执行本地事务
            doXXX();
            // 返回执行结果
            return LocalTransactionState.xxx;
        }

        /**
         * 消息回查执行的方法
         */
        @Override
        public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
			// 回查事务状态
            findxxx();
            // 消息来回查的时候,进行提交事务
            return LocalTransactionState.xxx;
        }
    });

    // 启动producer
    producer.start();
}

// 发送消息
SendResult result = producer.sendMessageInTransaction(msg, null);

和普通消息不同,在37行中使用sendMessageIntransaction()方法发送事务方法,那么发送的逻辑肯定都在这个方法下:

@Override
public TransactionSendResult sendMessageInTransaction(final Message msg, final Object arg) throws MQClientException {
    // 本地事务处理逻辑必须要先定义好
    if (null == this.transactionListener) {
        throw new MQClientException("TransactionListener is null", null);
    }

    // 对topic进行包装,附带延迟、重试标记
    msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic()));
    return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg);
}

进入sendMessageInTransaction()方法, 我们主要关注方法的前半部分:

public TransactionSendResult sendMessageInTransaction(final Message msg, final LocalTransactionExecuter localTransactionExecuter, final Object arg) throws MQClientException {
    TransactionListener transactionListener = getCheckListener();
    if (null == localTransactionExecuter && null == transactionListener) {
        throw new MQClientException("tranExecutor is null", null);
    }

    // ignore DelayTimeLevel parameter
    if (msg.getDelayTimeLevel() != 0) {
        MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
    }
    Validators.checkMessage(msg, this.defaultMQProducer);

    SendResult sendResult;
    // 给消息打上TRAN_MSG标记,标记为事务消息
    MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
    // 标记生产者组的目的是broker进行回查时需要
    MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
    try {
        // 发送half消息
        sendResult = this.send(msg);
    } catch (Exception e) {
        throw new MQClientException("send message Exception", e);
    }
    
    // 略
}

可以看到代码里给消息添加了两个属性:

  • PROPERTY_TRANSACTION_PREPARED : 标记为事务消息,broker可以根据msg是否有该字段来判断是否是事务消息。
  • PROPERTY_PRODUCER_GROUP : 生产者组,这个在broker进行事务结果回查时候需要。

在对消息进行特殊包装变成事务消息后,调用的send()方法就是通用的消息发送方法了,所有消息都是通过这个方法进行发送的。

Broker 处理 Half 消息

在 broker/src/main/java/org/apache/rocketmq/broker/processor/SendMessageProcessor.java 类中,asyncSendMessage()方法中有一段代码:

...
// 获取事务属性字段
String transFlag = origProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
// 判断是否为空 && 值为true
if (transFlag != null && Boolean.parseBoolean(transFlag)) {
  if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) {
    response.setCode(ResponseCode.NO_PERMISSION);
    response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + "] sending transaction message is forbidden");
    return CompletableFuture.completedFuture(response);
  }
  // 存储prepare消息
  putMessageResult = this.brokerController.getTransactionalMessageService().asyncPrepareMessage(msgInner);
} else {
  // 存储普通消息
  putMessageResult = this.brokerController.getMessageStore().asyncPutMessage(msgInner);
}
...

看看如果处理prepare消息的。

@Override
public CompletableFuture<PutMessageResult> asyncPrepareMessage(MessageExtBrokerInner messageInner) {
    return transactionalMessageBridge.asyncPutHalfMessage(messageInner);  //1 调用asyncPutHalfMessage()方法
}

asyncPrepareMessage()调用asyncPutHalfMessage()方法

public CompletableFuture<PutMessageResult> asyncPutHalfMessage(MessageExtBrokerInner messageInner) {
    return store.asyncPutMessage(parseHalfMessageInner(messageInner));
}

store.asyncPutMessage()就是通用的方法,普通消息的存储也是使用这个方法,所以half消息的特殊处理在parseHalfMessageInner()

private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
    // 备份消息的原主题和原队列ID
    MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
    MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msgInner.getQueueId()));
    msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));
    // 事务消息的topic和queueID是写死固定的
    msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
    msgInner.setQueueId(0);
    msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
    return msgInner;
}

分为两部分逻辑

  1. 备份原有的topic和queueId,将消息真正的topic放在REAL_TOPIC属性中,queueId放在REAL_QID属性中
  2. 使用事务专用的topic和queueId覆盖旧值,topic = RMQ_SYS_TRANS_HALF_TOPIC,queueId = 0。这样意味着所有的half消息都会存在同一个topic队列中。

疑问解答

现在可以回答之前的疑惑了

  1. producer 如何发送 half 消息的?

答:RocketMQ对于事务消息,使用了专门的Producer对象TransactionMQProducer,这个producer的发送消息的方法把消息包装成事务消息。

  1. broker 又是如何区分普通消息和事务消息的

答:在消息发送的时候,在消息的 property里添加PROPERTY_TRANSACTION_PREPARED标记,broker根据和这个标记来区分普通消息和事务消息。

  1. half消息不会被Consumer消费,是如何实现的?

答:消息经过特殊处理,都被分配到特有的topic队列中,被隔离了。这样消费者就无法获取到这些消息了。