本文章基于 RocketMQ 4.9.3
1. 前言
- 【RocketMQ】- 源码系列目录
- 【RocketMQ 生产者消费者】- 同步、异步、单向发送消费消息
- 【RocketMQ 生产者和消费者】- 消费者启动源码
- 【RocketMQ 生产者和消费者】- 消费者重平衡(1)
- 【RocketMQ 生产者和消费者】- 消费者重平衡(2)- 分配策略
- 【RocketMQ 生产者和消费者】- 消费者重平衡(3)- 消费者 ID 对负载均衡的影响
- 【RocketMQ 生产者和消费者】- 消费者的订阅关系一致性
- 【RocketMQ 生产者和消费者】- 消费者发起消息拉取请求 PullMessageService
- 【RocketMQ 生产者和消费者】- broker 是如何处理消费者消息拉取的 Netty 请求的
- 【RocketMQ 生产者和消费者】- broker 处理消息拉取请求
- 【RocketMQ 生产者和消费者】- 消费者处理消息拉取结果
- 【RocketMQ 生产者和消费者】- 消费者处理消息拉取结果
- 【RocketMQ 生产者和消费者】- ConsumeMessageConcurrentlyService 并发消费消息
- 【RocketMQ 生产者和消费者】- ConsumeMessageOrderlyService 顺序消费消息
- 【RocketMQ 生产者和消费者】- sendMessageBack 发送重试消息
- 【RocketMQ 生产者和消费者】- 延时消息的使用
- 【RocketMQ 生产者和消费者】- 延时消息原理解析-ScheduleMessageService
- 【RocketMQ 生产者和消费者】- 事务消息的使用
上一篇文章讲述了事务消息的使用,这篇文章来看下事务消息的实现原理,基本步骤就是下面几步,我把上一篇文章的图贴过来方便对着源码分析。

2. 发送事务消息
先看下半事务消息的发送,就是 sendMessageInTransaction 方法。
java
/**
* 发送事务消息
* @param msg 要发送的事务消息
* @param arg 用来执行本地事务方法传入的参数, 也就是 executeLocalTransaction 方法
* @return
* @throws MQClientException
*/
@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);
}
可以看到这里也是会判断如果事务监听器没有设置,就会抛出异常,这里的监听器就是上一篇文章中设置用来执行本地事务以及执行本地事务回查的。
java
/**
* 发送事务消息
* @param msg 要发送的事务消息
* @param localTransactionExecuter 本地事务执行器, 现在已经弃用了
* @param arg 事务监听器执行本地事务的时候传入的参数
* @return
* @throws MQClientException
*/
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);
}
// 事务消息不支持延时消息
if (msg.getDelayTimeLevel() != 0) {
MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
}
// 检查要发送的消息的合法性, 就是校验 topic 和消息大小
Validators.checkMessage(msg, this.defaultMQProducer);
SendResult sendResult = null;
// 设置事务半消息标记, 也就是 TRAN_MSG = true
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
// 设置生产者组
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
try {
// 1. 发送半事务消息, 这里会将消息发送到对应的 CommitLog 中
sendResult = this.send(msg);
} catch (Exception e) {
throw new MQClientException("send message Exception", e);
}
// 2. 发送完成后开始执行本地事务
LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
Throwable localException = null;
switch (sendResult.getSendStatus()) {
// 发送半消息成功
case SEND_OK: {
try {
// 获取事务 id
if (sendResult.getTransactionId() != null) {
// 将事务 id 设置到 __transactionId__ 这个属性中
msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
}
// 获取消息 id
String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
if (null != transactionId && !"".equals(transactionId)) {
// 设置这条消息的事务 id 为上面的消息 id
msg.setTransactionId(transactionId);
}
if (null != localTransactionExecuter) {
// 如果存在本地事务执行器, 就执行 executeLocalTransactionBranch
localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
} else if (transactionListener != null) {
// 如果不存在, 就通过事务监听器执行 executeLocalTransaction, 现在一般走这种方式
log.debug("Used new transaction API");
localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
}
if (null == localTransactionState) {
// 如果没有返回结果, 将状态当成 UNKNOW
localTransactionState = LocalTransactionState.UNKNOW;
}
// 如果不是 COMMIT_MESSAGE, 打印日志, 可能是本地事务有什么问题
if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
log.info("executeLocalTransactionBranch return {}", localTransactionState);
log.info(msg.toString());
}
} catch (Throwable e) {
log.info("executeLocalTransactionBranch exception", e);
log.info(msg.toString());
localException = e;
}
}
break;
// 刷盘超时
case FLUSH_DISK_TIMEOUT:
// 从节点同步超时
case FLUSH_SLAVE_TIMEOUT:
// 从节点不可用
case SLAVE_NOT_AVAILABLE:
// 这几种状态都设置本地事务的执行结果为 ROLLBACK_MESSAGE, 回滚这条消息
localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
break;
default:
break;
}
try {
// 3. 本地事务执行完成, 根据执行结果 commit 或者 rollback
this.endTransaction(msg, sendResult, localTransactionState, localException);
} catch (Exception e) {
log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
}
// 最终返回事务发送结果
TransactionSendResult transactionSendResult = new TransactionSendResult();
transactionSendResult.setSendStatus(sendResult.getSendStatus());
transactionSendResult.setMessageQueue(sendResult.getMessageQueue());
transactionSendResult.setMsgId(sendResult.getMsgId());
transactionSendResult.setQueueOffset(sendResult.getQueueOffset());
transactionSendResult.setTransactionId(sendResult.getTransactionId());
transactionSendResult.setLocalTransactionState(localTransactionState);
return transactionSendResult;
}
上面就是生产者的事务执行逻辑,如果按照上面的图来对照就是执行了 1、2、3、4 步,可以看下里面的逻辑,一些关键点都写了注释。
可以看到设置事务消息之前会先设置下半消息的标记,也就是属性 TRAN_MSG 设置为 true,然后将生产者组设置到 PGROUP 属性中,然后调用 send 发送半事务消息,这个 send 方法就是之前生产者发送的源码,也有解析过,可以看之前的文章,不过后面还是会讲一下 send 方法里面对于事务消息的处理。
当消息发送成功后,获取消息属性里面的 UNIQ_KEY,这个 UNIQ_KEY 是客户端生产者发送消息的时候生成的唯一 ID,事务 ID 就用这个 ID 来设置,设置好了之后开始执行本地事务,也就是上面图中的第三点,这里执行结果如果没有返回,那么就是 UNKNOW,这个就代表事务执行结果不确定,后面是会事务回查的。
最后事务执行完成,调用 endTransaction 来处理事务执行结果。
2.1 消息发送对事务消息的处理
首先是 sendKernelImpl,这个方法就是生产者同步、异步、单向发送消息的其中一个核心方法,这里面会获取事务标记 PROPERTY_TRANSACTION_PREPARED,也就是上面 sendMessageInTransaction 方法设置的属性,默认就是 true。这里会在系统标记上加上 TRANSACTION_PREPARED_TYPE,代表这条事务消息是一条半事务消息。

接下来在 broker 处理生产者的消息时,也就是 asyncSendMessage 方法,在这里面会去判断是否是事务消息,如果是,就会走半事务消息的添加逻辑。

跟着源码一直进去会发现这个方法跟普通添加的方法区别不大,只是这里会将原始消息转成半事务消息来添加,主要是 parseHalfMessageInner 这个方法。
2.2 parseHalfMessageInner 解析原始消息为半事务消息
java
/**
* 解析消息为半事务消息
* @param msgInner 原始消息
* @return
*/
private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
// 将真实 topic 写入 REAL_TOPIC 属性中
MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
// 将真实 queueId 写入 REAL_QID 属性中
MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
String.valueOf(msgInner.getQueueId()));
// 将 TRANSACTION_ROLLBACK_TYPE 从系统标记中删掉
msgInner.setSysFlag(
MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));
// 半事务消息默认发送到 RMQ_SYS_TRANS_HALF_TOPIC 下面的 0 队列
msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
msgInner.setQueueId(0);
// 设置属性值
msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
return msgInner;
}
这里就是将原始消息转为半消息,事务半消息都要往 RMQ_SYS_TRANS_HALF_TOPIC 这个 topic 去添加,只有一个队列,id 就是 0,因此跟延时消息一样,需要将真实的 topic 和 queueId 设置到消息属性 REAL_TOPIC 和 REAL_QID 中,然后将事务消息的状态重置成 TRANSACTION_NOT_TYPE,表示这条 half 消息本地事务还没有执行结果。剩下的流程就是将这条消息发送到对应的 topic 下面的 queue 了。
3. 处理本地事务执行结果
3.1 endTransaction 发送本地事务执行结果到 broker
java
/**
* 本地事务处理完成, 处理结果
* @param msg 要发送的事务消息
* @param sendResult 消息发送结果
* @param localTransactionState 本地事务执行结果
* @param localException 本地事务执行过程中发生的异常
* @throws RemotingException
* @throws MQBrokerException
* @throws InterruptedException
* @throws UnknownHostException
*/
public void endTransaction(
final Message msg,
final SendResult sendResult,
final LocalTransactionState localTransactionState,
final Throwable localException) throws RemotingException, MQBrokerException, InterruptedException, UnknownHostException {
final MessageId id;
// 解码消息 ID, 如果存在 offsetMsgId, 就解析 offsetMsgId, offsetMsgId 是由 Broker 服务端在写入消息时生成的(采用 "IP 地址 + Port 端口" 与 "CommitLog 的物理偏移量地址"做了一个字符串拼接),
// 其中 offsetMsgId 就是在 RocketMQ 控制台直接输入查询的那个 messageId
if (sendResult.getOffsetMsgId() != null) {
id = MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId());
} else {
// 不存在就解析消息 ID, 也就是 UniqID, 是由客户端 producer 实例端生成的,具体来说,调用方法 `MessageClientIDSetter.createUniqIDBuffer()` 生成唯一的 Id
id = MessageDecoder.decodeMessageId(sendResult.getMsgId());
}
// 获取事务 ID
String transactionId = sendResult.getTransactionId();
// 根据 brokerName 获取这个 broker 集群下面的主节点地址
final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName());
// 创建请求头
EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader();
// 设置事务 id
requestHeader.setTransactionId(transactionId);
// 设置消息的 CommitLog 偏移量
requestHeader.setCommitLogOffset(id.getOffset());
// 设置本地事务状态
switch (localTransactionState) {
case COMMIT_MESSAGE:
requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE);
break;
case ROLLBACK_MESSAGE:
requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE);
break;
case UNKNOW:
requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE);
break;
default:
break;
}
// 执行钩子方法, 也就是 EndTransactionHook#endTransaction
doExecuteEndTransactionHook(msg, sendResult.getMsgId(), brokerAddr, localTransactionState, false);
// 设置生产者组
requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
// 设置事务消息的偏移量(ConsumeQueue)
requestHeader.setTranStateTableOffset(sendResult.getQueueOffset());
// 设置消息 id
requestHeader.setMsgId(sendResult.getMsgId());
// 异常描述
String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null;
this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark,
this.defaultMQProducer.getSendMsgTimeout());
}
public void endTransactionOneway(
final String addr,
final EndTransactionRequestHeader requestHeader,
final String remark,
final long timeoutMillis
) throws RemotingException, MQBrokerException, InterruptedException {
// 请求 Code 是 END_TRANSACTION
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, requestHeader);
// 设置异常描述
request.setRemark(remark);
this.remotingClient.invokeOneway(addr, request, timeoutMillis);
}
首先解码消息 ID,如果存在 offsetMsgId,就解析 offsetMsgId,offsetMsgId 是由 Broker 服务端在写入消息时生成的(采用 IP 地址 + Port 端口 与 CommitLog 的物理偏移量地址"做了一个字符串拼接),offsetMsgId 就是在 RocketMQ 控制台直接输入查询的那个 messageId。
如果不存在就解析消息 ID,也就是 UniqID, 是由客户端 producer 实例端生成的,具体来说,调用方法 MessageClientIDSetter.createUniqIDBuffer() 生成唯一的 Id。
接下来获取事务 ID,transactionId 和 上面的 UniqID 是一样的。然后获取 brokerAddr,接下来根据本地事务执行返回值设置对应的结果。
- COMMIT_MESSAGE -> TRANSACTION_COMMIT_TYPE
- ROLLBACK_MESSAGE -> TRANSACTION_ROLLBACK_TYPE
- UNKNOW -> TRANSACTION_NOT_TYPE
END_TRANSACTION 就是请求 Code,而且这里发送的是一个单向请求,不需要 broker 的响应,下面来看下 broker 是如何处理的。
3.2 EndTransactionProcessor 处理生产者本地事务执行结果
broker 通过 EndTransactionProcessor 来处理事务执行结果,核心逻辑在 processRequest 中。
java
@Override
public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws
RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
// 1.解析出请求头
final EndTransactionRequestHeader requestHeader =
(EndTransactionRequestHeader)request.decodeCommandCustomHeader(EndTransactionRequestHeader.class);
LOGGER.debug("Transaction request:{}", requestHeader);
// 从节点不支持事务消息
if (BrokerRole.SLAVE == brokerController.getMessageStoreConfig().getBrokerRole()) {
response.setCode(ResponseCode.SLAVE_NOT_AVAILABLE);
LOGGER.warn("Message store is slave mode, so end transaction is forbidden. ");
return response;
}
// 2.判断是不是生产者发送过来的事务回查请求
if (requestHeader.getFromTransactionCheck()) {
// 判断事务回查结果
switch (requestHeader.getCommitOrRollback()) {
// 事务回查还没有结果, 这时候 broker 什么都不做, 直接结束
case MessageSysFlag.TRANSACTION_NOT_TYPE: {
LOGGER.warn("Check producer[{}] transaction state, but it's pending status."
+ "RequestHeader: {} Remark: {}",
RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
requestHeader.toString(),
request.getRemark());
return null;
}
// 事务回查结果是 commit, 就会提交这个消息
case MessageSysFlag.TRANSACTION_COMMIT_TYPE: {
LOGGER.warn("Check producer[{}] transaction state, the producer commit the message."
+ "RequestHeader: {} Remark: {}",
RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
requestHeader.toString(),
request.getRemark());
break;
}
// 事务回查结果为 rollback, 将会回滚该消息
case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: {
LOGGER.warn("Check producer[{}] transaction state, the producer rollback the message."
+ "RequestHeader: {} Remark: {}",
RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
requestHeader.toString(),
request.getRemark());
break;
}
default:
return null;
}
} else {
// 这里是生产者本地事务执行完成后发送的事务结束消息
switch (requestHeader.getCommitOrRollback()) {
// 一样没有结果, broker 不做任何处理
case MessageSysFlag.TRANSACTION_NOT_TYPE: {
LOGGER.warn("The producer[{}] end transaction in sending message, and it's pending status."
+ "RequestHeader: {} Remark: {}",
RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
requestHeader.toString(),
request.getRemark());
return null;
}
// 如果是 commit, 说明本地事务执行成功, 提交消息
case MessageSysFlag.TRANSACTION_COMMIT_TYPE: {
break;
}
// 如果是 rollback, 说明本地事务执行失败, 回滚消息
case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: {
LOGGER.warn("The producer[{}] end transaction in sending message, rollback the message."
+ "RequestHeader: {} Remark: {}",
RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
requestHeader.toString(),
request.getRemark());
break;
}
default:
return null;
}
}
// 3.事务提交
// 到这里就是本地事务执行成功或者事务回查成功, 提交
OperationResult result = new OperationResult();
if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
// 这里会从 CommitLog 中查询出半事务消息
result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
// 查询成功
if (result.getResponseCode() == ResponseCode.SUCCESS) {
// 半事务消息检查
RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
if (res.getCode() == ResponseCode.SUCCESS) {
// 根据半事务消息还原出原始的消息
MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
// 设置消息的系统标记
msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback()));
// 设置消息的 ConsumeQueue 偏移量
msgInner.setQueueOffset(requestHeader.getTranStateTableOffset());
// 设置消息所在 CommitLog 的偏移量
msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());
// 设置消息的存储时间
msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());
// 清除半事务消息标记
MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED);
// 将原始消息发送到对应的 topic
RemotingCommand sendResult = sendFinalMessage(msgInner);
if (sendResult.getCode() == ResponseCode.SUCCESS) {
// 发送成功, 将半事务消息删掉
this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
}
return sendResult;
}
return res;
}
// 4.事务回滚
// 到这里就是本地事务执行结果是 ROLLBACK_MESSAGE 或者事务回查结果是 ROLLBACK_MESSAGE
} else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
// 这里也是根据请求头查询出对应的半事务消息
result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);
// 获取成功
if (result.getResponseCode() == ResponseCode.SUCCESS) {
// 半事务消息检测
RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
if (res.getCode() == ResponseCode.SUCCESS) {
// 删掉这条需要回滚的消息
this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
}
return res;
}
}
// 返回结果
response.setCode(result.getResponseCode());
response.setRemark(result.getResponseRemark());
return response;
}
可以看下上面的注释,上面流程主要是分为了四部分,首先是解析请求头 requestHeader,然后判断当前节点是不是从节点,如果是从节点就不支持事务。
第二部分,判断是不是生产者发送过来的事务回查请求,如果是 broker 通过定时任务检测到在事务超时时间内没有收到生产者的本地事务执行结果,就会发起回查请求,生产者回查之后会将结果发送给 broker,也是调用这个方法,这时候 fromTransactionCheck 就是 true 了,这种情况下就会进入这里面的判断,可以看到就是如果生产者本地事务回查之后还是返回 TRANSACTION_NOT_TYPE,那么直接退出这个方法,不做处理。如果是第一次生产者执行本地事务之后主动上传结果的就会进入 else。
可以看到这两个分支的判断基本相同,就是日志输出不同,都是遇到 TRANSACTION_NOT_TYPE 就直接返回。
接下来第三部分,判断事务提交结果,根据提交结果是 TRANSACTION_COMMIT_TYPE 还是 TRANSACTION_ROLLBACK_TYPE 走不同的分支。
TRANSACTION_COMMIT_TYPE:调用 commitMessage 提交消息,其实里面的实现就是从 commitLog 中查询出 half 消息。查询成功之后还原出原始的消息,上面 2.2 小节就已经说过了,从原始消息到 half 消息会把原始 topic 和 queueId 存在消息属性中,所以这里的还原endMessageTransaction也是差不多的逻辑,等会下面再看。还原出原始消息之后将原始消息通过sendFinalMessage方法发送到真实的 topic 中,这下消费者就可以消费了。发送成功将 half 消息删掉。TRANSACTION_ROLLBACK_TYPE:逻辑差不多,只是说对于 rollback 就不会将消息再重新存到真实 topic 中了,而是直接将消息删掉。
3.3 commitMessage&rollbackMessage 从 commitLog 中查询出 half 消息
java
@Override
public OperationResult commitMessage(EndTransactionRequestHeader requestHeader) {
return getHalfMessageByOffset(requestHeader.getCommitLogOffset());
}
这里是根据 commitLogOffset 获取 half 消息。
java
/**
* 根据偏移量从 CommitLog 中通过 DefaultMessageStore#lookMessageByOffset 查询出半事务消息
* @param commitLogOffset
* @return
*/
private OperationResult getHalfMessageByOffset(long commitLogOffset) {
OperationResult response = new OperationResult();
MessageExt messageExt = this.transactionalMessageBridge.lookMessageByOffset(commitLogOffset);
if (messageExt != null) {
response.setPrepareMessage(messageExt);
response.setResponseCode(ResponseCode.SUCCESS);
} else {
response.setResponseCode(ResponseCode.SYSTEM_ERROR);
response.setResponseRemark("Find prepared transaction message failed");
}
return response;
}
lookMessageByOffset 根据 commitLogOffset 中获取到这条消息,里面也是调用了 CommitLog#getMessage 方法去获取 half 消息。
java
/**
* 根据 commitLogOffset 中获取到这条消息
* @param commitLogOffset physical offset.
* @return
*/
public MessageExt lookMessageByOffset(long commitLogOffset) {
// 首先获取下从 commitLogOffset 开始的 4 字节的 ByteBuffer
SelectMappedBufferResult sbr = this.commitLog.getMessage(commitLogOffset, 4);
if (null != sbr) {
try {
// 由于 CommitLog 中存储消息的第一个信息就是 4 字节的 TOTALSIZE, 也就是消息的总大小
int size = sbr.getByteBuffer().getInt();
// 所以获取到 size 之后, 再次从 commitLogOffset 开始截取 size 大小的完整消息
return lookMessageByOffset(commitLogOffset, size);
} finally {
sbr.release();
}
}
return null;
}
/**
* 从 commitLogOffset 开始截取 size 长度的消息
* @param commitLogOffset
* @param size
* @return
*/
public MessageExt lookMessageByOffset(long commitLogOffset, int size) {
// 从 CommitLog 中 commitLogOffset 偏移量开始获取长度为 size 的 ByteBuffer
SelectMappedBufferResult sbr = this.commitLog.getMessage(commitLogOffset, size);
if (null != sbr) {
try {
// 解码成一条消息
return MessageDecoder.decode(sbr.getByteBuffer(), true, false);
} finally {
sbr.release();
}
}
return null;
}
这里由于不清楚 half 消息的具体大小是多少,先从 commitLogOffset 开始查询四字节大小,可以在这篇文章看下 CommitLog 的结构:【RocketMQ 存储】- 一文总结 RocketMQ 的存储结构-基础。一条 CommitLog 消息开头就是 4 字节的总长度,最后再次调用 lookMessageByOffset 从 commitLogOffset 开始截取 size 大小的 ByteBuffer,这里就是一条完整的消息了。
getMessage 获取 size 大小的 ByteBuffer,封装到 SelectMappedBufferResult 返回。
java
public SelectMappedBufferResult getMessage(final long offset, final int size) {
// 获取 CommitLog 一个文件大小 1GB
int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog();
// 根据偏移量找到对应的 MappedFile
MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset, offset == 0);
if (mappedFile != null) {
// 从 pos 开始截取一段 ByteBuffer,这段 ByteBuffer 的大小是 pos 到 MappedFile 文件尾部
int pos = (int) (offset % mappedFileSize);
return mappedFile.selectMappedBuffer(pos, size);
}
// 没找到就返回空
return null;
}
上面核心截取 ByteBuffer 的逻辑在方法 selectMappedBuffer 中,感兴趣可以看这篇文章:【RocketMQ 存储】- RocketMQ存储类 MappedFile。
3.4 checkPrepareMessage 检查 half 消息是否合法
在提交半消息或者删掉半消息之前需要检查下 half 消息是否合法,得跟生产者发送过来的请求对的上,就是在 CommitLog 和 ConsumeQueue 中的偏移量对的上才能删除,别删错了。
java
/**
* 半事务消息检查
* @param msgExt
* @param requestHeader
* @return
*/
private RemotingCommand checkPrepareMessage(MessageExt msgExt, EndTransactionRequestHeader requestHeader) {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
if (msgExt != null) {
// 获取生产者组
final String pgroupRead = msgExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP);
if (!pgroupRead.equals(requestHeader.getProducerGroup())) {
// 如果这条半事务消息的生产者组和请求头里面的生产者组不一样, 返回 SYSTEM_ERROR
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("The producer group wrong");
return response;
}
if (msgExt.getQueueOffset() != requestHeader.getTranStateTableOffset()) {
// 如果这条半事务消息的 ConsumeQueue 偏移量和请求头里面的偏移量不一样, 返回 SYSTEM_ERROR
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("The transaction state table offset wrong");
return response;
}
if (msgExt.getCommitLogOffset() != requestHeader.getCommitLogOffset()) {
// 如果这条半事务消息的 CommitLog 偏移量和请求头里面的偏移量不一样, 返回 SYSTEM_ERROR
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("The commit log offset wrong");
return response;
}
} else {
// 这里是参数为空
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("Find prepared transaction message failed");
return response;
}
response.setCode(ResponseCode.SUCCESS);
return response;
}
3.5 endMessageTransaction 还原出原始消息
java
/**
* 还原原始的消息
* @param msgExt half 消息
* @return
*/
private MessageExtBrokerInner endMessageTransaction(MessageExt msgExt) {
// 创建消息
MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
// 首先从半事务消息的属性中获取出真实投递的 topic
msgInner.setTopic(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC));
// 再获取出真实投递的 queueId
msgInner.setQueueId(Integer.parseInt(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_QUEUE_ID)));
msgInner.setBody(msgExt.getBody());
msgInner.setFlag(msgExt.getFlag());
msgInner.setBornTimestamp(msgExt.getBornTimestamp());
msgInner.setBornHost(msgExt.getBornHost());
msgInner.setStoreHost(msgExt.getStoreHost());
msgInner.setReconsumeTimes(msgExt.getReconsumeTimes());
msgInner.setWaitStoreMsgOK(false);
// 设置事务 ID, 就是消息 ID, 属性 UNIQ_KEY
msgInner.setTransactionId(msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX));
msgInner.setSysFlag(msgExt.getSysFlag());
// 生成 tagsCode
TopicFilterType topicFilterType =
(msgInner.getSysFlag() & MessageSysFlag.MULTI_TAGS_FLAG) == MessageSysFlag.MULTI_TAGS_FLAG ? TopicFilterType.MULTI_TAG
: TopicFilterType.SINGLE_TAG;
long tagsCodeValue = MessageExtBrokerInner.tagsString2tagsCode(topicFilterType, msgInner.getTags());
msgInner.setTagsCode(tagsCodeValue);
MessageAccessor.setProperties(msgInner, msgExt.getProperties());
msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
// 清空半事务消息的属性
MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC);
MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID);
return msgInner;
}
首先就是 queueId 和 topic 的还原,直接从属性 REAL_TOPIC 和 REAL_QID 中拿出来,然后就是事务 ID,事务 ID 就是上面说的消息 ID,生产者发送的时候生成的 UNIQ_KEY,最后手动生成 tagsCode,清空事务属性,这个方法就结束了,但是到这里原始消息还没有设置完,可以看到 3.2 小节的上层调用,获取到 msg 之后,还需要设置下面属性。
- 消息的系统标记,commit 或者 rollback 的事务标记
- 消息的 ConsumeQueue 偏移量,使用 half 消息的
- half 消息所在 CommitLog 的偏移量
- 消息的存储时间
- 清除半事务消息标记,避免 broker 在处理这条消息的时候又当成 half 消息去处理了
3.6 消息的 ConsumeQueue 偏移量设置
上面可以看到当 half 消息 commit 之后,还原出来的原始消息会把 half 消息的 ConsumeQueue 偏移量也设置进去,这里可能有人会有疑问,如果把 half 消息的偏移量当成原始消息的偏移量,那么在构建 ConsumeQueue 索引的时候不会因为判断这个 queueOffset 小于已经当前已经构造出的 ConsumeQueue 索引的最大值就返回吗?

上面在写入 ConsumeQueue 的时候如果 expectLogicOffset 小于当前 ConsumeQueue 下一个要写入的偏移量,就代表这条索引已经构建过,直接返回。所以如果当前 half 消息写入真实队列的时候如果转换之后的原始消息用的是 half 消息的偏移量,会不会出现上面这种问题,毕竟 half 消息跟原始消息是两个队列。
这里就涉及到 CommitLog 的写入逻辑了,主要是 doAppend 方法。

这里在写入之前会从 topicQueueTable 中获取 queueOffset,如果不存在就初始化为 0,这个 key = topic-queueId,然后再写入之前会将这个 queueOffset 重新写回消息的 buffer。

因此虽然 half 消息存入真实 topic 的时候会将 half 消息的 queueOffset 设置到真实消息的 queueOffset 属性,再写入 CommitLog,由于重放也是按顺序重放的,所以真实消息写入的时候是可以确保被构建成 ConsumeQueue 索引的(消息过滤用)。
下面我也简单做了一个测试,生产者发送 half 消息,存到 CommitLog 之后输出这条消息的 queueOffset。

接下来 broker 我也在控制台输出了一些信息,可以看下。

首先就是 half 消息写入的时候消息的 queueOffset = 7,然后消息重放服务构建 ConsumeQueue 的时候从 CommitLog 里面读出的消息的 queueOffset = 7,这就是第 1、2 个红框的输出。
生产者发送 half 消息成功之后,执行本地事务,生产者我改了下,只会发送一条消息,同时本地事务执行方法 executeLocalTransaction 统一返回 LocalTransactionState.COMMIT_MESSAGE,因此可以看到第三个框,当 half 消息执行成功之后,往 CommitLog 里面发送真实消息,注意这里的消息 queueOffset 输出我是打印到了 CommitLog#doAppend 方法,因此 half 消息转成真实消息的 queueOffset = 7 没问题,但是最终写入的时候 queueOffset 还是从 topicQueueTable 中获取的,就是 2。

因此最终消息重放出来的 queueOffset 就是 2,真实 topic 是 TopicTest1234,queueId = 3。最后一个红框是 half 消息 commit 之后通过写入一条 op 消息来标记这条 half 本地事务已经执行完成。
3.7 sendFinalMessage 将消息发送到原始 topic 中
java
/**
* 发送原始消息到 CommitLog 中
* @param msgInner
* @return
*/
private RemotingCommand sendFinalMessage(MessageExtBrokerInner msgInner) {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
// 将消息添加到 CommitLog 中
final PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
if (putMessageResult != null) {
switch (putMessageResult.getPutMessageStatus()) {
// Success
case PUT_OK:
case FLUSH_DISK_TIMEOUT:
case FLUSH_SLAVE_TIMEOUT:
case SLAVE_NOT_AVAILABLE:
// 写入主节点成了, 上面这些都是从节点的问题, 第二个 FLUSH_DISK_TIMEOUT 虽然超时, 但是消息是已经写入 MappedByteBuffer 了
// 也就是已经写入 PageCache, 只差最后一步, 后面再次去刷盘就行
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
break;
// Failed
case CREATE_MAPEDFILE_FAILED:
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("Create mapped file failed.");
break;
case MESSAGE_ILLEGAL:
case PROPERTIES_SIZE_EXCEEDED:
response.setCode(ResponseCode.MESSAGE_ILLEGAL);
response.setRemark("The message is illegal, maybe msg body or properties length not matched. msg body length limit 128k, msg properties length limit 32k.");
break;
case SERVICE_NOT_AVAILABLE:
response.setCode(ResponseCode.SERVICE_NOT_AVAILABLE);
response.setRemark("Service not available now.");
break;
case OS_PAGECACHE_BUSY:
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("OS page cache busy, please try another machine");
break;
case UNKNOWN_ERROR:
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("UNKNOWN_ERROR");
break;
default:
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("UNKNOWN_ERROR DEFAULT");
break;
}
return response;
} else {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("store putMessage return null");
}
return response;
}
/**
* 消息存储
* @param msg Message instance to store
* @return
*/
@Override
public PutMessageResult putMessage(MessageExtBrokerInner msg) {
// asyncPutMessage 异步存储消息, 返回结果是 CompletableFuture<PutMessageResult>
return waitForPutResult(asyncPutMessage(msg));
}
这个方法逻辑比较简单,就是调用 DefaultMessageStore#putMessage 来添加消息,实际上就是最终会调用 asyncPutMessage 来添加,这里的逻辑就不多说了,如果感兴趣可以看这篇文章:【RocketMQ 生产者和消费者】- broker 处理生产者发送的消息。
3.8 deletePrepareMessage 删除 half 消息
RocketMQ 无法真正删除一条消息,消息顺序写入 CommitLog 之后就会刷盘,所以这里用的是删除标记,RocketMQ 引入了 Op 消息,用这条消息标记一条半事务消息的删除状态,不管是 Commit 或者是 Rollback 最终都会调用这个方法,也就是说如果一条消息找不到对应的 Op 消息,就说明这条消息没办法确认是 Commit 还是 Rollback,这时候就要发起事务回查,让生产者去检查本地事务。
deletePrepareMessage 方法就是用于删除 half,也就是往 CommitLog 里面写入一条 op 消息。
java
/**
* 删除 half 消息
* @param msgExt
* @return
*/
@Override
public boolean deletePrepareMessage(MessageExt msgExt) {
// RocketMQ 无法真正删除一条消息, 消息顺序写入 CommitLog 之后就会刷盘, 所以这里用的是删除标记
// RocketMQ 引入了 Op 消息, 用这条消息标记一条半事务消息的删除状态, 不管是 Commit 或者是 Rollback 最终都会调用这个方法, 也就是说
// 如果一条消息找不到对应的 Op 消息, 就说明这条消息没办法确认是 Commit 还是 Rollback, 这时候就要发起事务回查
if (this.transactionalMessageBridge.putOpMessage(msgExt, TransactionalMessageUtil.REMOVETAG)) {
log.debug("Transaction op message write successfully. messageId={}, queueId={} msgExt:{}", msgExt.getMsgId(), msgExt.getQueueId(), msgExt);
return true;
} else {
log.error("Transaction op message write failed. messageId is {}, queueId is {}", msgExt.getMsgId(), msgExt.getQueueId());
return false;
}
}
/**
* 添加 Op 消息
* @param messageExt half 消息
* @param opType op 消息标记 d
* @return
*/
public boolean putOpMessage(MessageExt messageExt, String opType) {
// 创建一个 MessageQueue, topic 是半事务消息的 topic, 也就是 RMQ_SYS_TRANS_HALF_TOPIC, 然后队列 ID 是半事务消息对应的 ID, 默认是 0
MessageQueue messageQueue = new MessageQueue(messageExt.getTopic(),
this.brokerController.getBrokerConfig().getBrokerName(), messageExt.getQueueId());
if (TransactionalMessageUtil.REMOVETAG.equals(opType)) {
// 写入 Op 消息到 MessageQueue 中
return addRemoveTagInTransactionOp(messageExt, messageQueue);
}
return true;
}
这里由于 half 消息和 op 消息是一一对应的,所以 putOpMessage 会先创建出一个 MessageQueue,然后写入 Op 消息到 MessageQueue 中,当然这个 MessageQueue 不是 op 消息的队列,而是 half 消息的消息队列,最终也不是写入这个队列。这个 MessageQueue 是用来在 opQueueMap 中查出对应的 op 队列,如果不存在就创建一个。
那么这里每一次加入一条 op 消息都会创建一个 MessageQueue,会不会导致消息队列越来越多呢?这个不用担心,因为 MessageQueue 重写了 equals 和 hashCode 方法,因此这里就算有多条消息都创建了 MessageQueue,opQueueMap 也只会根据 topic、brokerName、queueId 获取一个 MessageQueue,由于 topic 是固定死的 RMQ_SYS_TRANS_HALF_TOPIC,队列 id 又是 0,所以只有在不同集群下 opQueueMap 才会存不同的 key,因为 brokerName 不同。
下面看下 addRemoveTagInTransactionOp 方法。
java
/**
* Use this function while transaction msg is committed or rollback write a flag 'd' to operation queue for the
* msg's offset
*
* @param prepareMessage Half message
* @param messageQueue Half message queue
* @return This method will always return true.
*/
private boolean addRemoveTagInTransactionOp(MessageExt prepareMessage, MessageQueue messageQueue) {
// 创建消息, topic 是 RMQ_SYS_TRANS_OP_HALF_TOPIC, tags 是 d
Message message = new Message(TransactionalMessageUtil.buildOpTopic(), TransactionalMessageUtil.REMOVETAG,
String.valueOf(prepareMessage.getQueueOffset()).getBytes(TransactionalMessageUtil.charset));
// 将消息写入对应的 op 队列
writeOp(message, messageQueue);
return true;
}
addRemoveTagInTransactionOp 会创建出 op 消息,topic 是 RMQ_SYS_TRANS_OP_HALF_TOPIC,tags 是 d,消息内容是 half 消息的偏移量,最后调用 writeOp 将消息写入对应的 op 队列。
java
/**
* 写入 op 消息到对应的队列
* @param message
* @param mq
*/
private void writeOp(Message message, MessageQueue mq) {
// 因为 MessageQueue 重写了 equals 和 hashCode 方法, 因此这里就算有多条消息都创建了 MessageQueue, opQueueMap 也只会根据
// topic、brokerName、queueId 获取一个 MessageQueue
MessageQueue opQueue;
if (opQueueMap.containsKey(mq)) {
// 这里是能获取到
opQueue = opQueueMap.get(mq);
} else {
// 这里是 opQueueMap 里面没有, 新建一个, 可以看到里面创建出来的 MessageQueue 的 topic 默认就是 RMQ_SYS_TRANS_OP_HALF_TOPIC
// queueId 和 brokerName 和 half 消息的保持一致
opQueue = getOpQueueByHalf(mq);
MessageQueue oldQueue = opQueueMap.putIfAbsent(mq, opQueue);
if (oldQueue != null) {
opQueue = oldQueue;
}
}
// 正常情况下应该不会走到这里, 可能是并发导致删掉了?
if (opQueue == null) {
opQueue = new MessageQueue(TransactionalMessageUtil.buildOpTopic(), mq.getBrokerName(), mq.getQueueId());
}
// 将 op 消息存入 RMQ_SYS_TRANS_OP_HALF_TOPIC 的消息队列中
putMessage(makeOpMessageInner(message, opQueue));
}
可以看到里面的逻辑就是上面说的,先根据上面创建的 half 消息 MessageQueue 找到 op 消息的消息队列,接着使用 makeOpMessageInner 方法去创建一个 MessageExtBrokerInner 用于写入 RMQ_SYS_TRANS_OP_HALF_TOPIC 中,这里的写入就是通过 putMessage 来写入了,这里也不多说,最后来看下创建 op 消息的方法 getOpQueueByHalf。
java
/**
* 通过 half 的消息队列获取 op 消息队列
* @param halfMQ
* @return
*/
private MessageQueue getOpQueueByHalf(MessageQueue halfMQ) {
MessageQueue opQueue = new MessageQueue();
// op 消息队列的 topic 是 RMQ_SYS_TRANS_OP_HALF_TOPIC
opQueue.setTopic(TransactionalMessageUtil.buildOpTopic());
// brokerName
opQueue.setBrokerName(halfMQ.getBrokerName());
// 半事务消息队列 ID, 默认是 0
opQueue.setQueueId(halfMQ.getQueueId());
return opQueue;
}
最终来总结下,当生产者执行完本地事务之后,如果是 commit 或者 rollback,都会将 half 消息给删掉,但是 CommitLog 属于顺序写入,所以没办法真正删除一条消息,所以 RocketMQ 引入了 op 消息,op 消息的 topic 是 RMQ_SYS_TRANS_OP_HALF_TOPIC,queueId = 0,和 half 消息一样都是一个队列顺序写入,消息内容是 half 消息的偏移量,后续判断一条 half 消息有没有提交就看这个消息所属的 op 消息存不存在就行。
4. 小结

这篇文章主要是分析了事务执行源码的 1、2、3、4 部分,下一篇文章就来看下 broker 如果没有在规定时间内收到事务消息的执行结果,是如何发起回查的,生产者又是怎么执行事务回查的。
如有错误,还原指出!!!!