前言
half消息属于RocketMQ事务处理的第一个阶段,可以包括两部分:
- producer -> broker 发送half消息
- broker 处理half消息
对应图中的步骤1、2。
带着问题看源码, 先提几个问题:
- producer 如何发送 half 消息的?
- broker 又是如何区分普通消息和事务消息的
- 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;
}
分为两部分逻辑
- 备份原有的topic和queueId,将消息真正的topic放在
REAL_TOPIC属性中,queueId放在REAL_QID属性中 - 使用事务专用的topic和queueId覆盖旧值,topic =
RMQ_SYS_TRANS_HALF_TOPIC,queueId = 0。这样意味着所有的half消息都会存在同一个topic队列中。
疑问解答
现在可以回答之前的疑惑了
- producer 如何发送 half 消息的?
答:RocketMQ对于事务消息,使用了专门的Producer对象TransactionMQProducer,这个producer的发送消息的方法把消息包装成事务消息。
- broker 又是如何区分普通消息和事务消息的
答:在消息发送的时候,在消息的 property里添加PROPERTY_TRANSACTION_PREPARED标记,broker根据和这个标记来区分普通消息和事务消息。
- half消息不会被Consumer消费,是如何实现的?
答:消息经过特殊处理,都被分配到特有的topic队列中,被隔离了。这样消费者就无法获取到这些消息了。