RocketMQ架构篇 - 事务消息

本文深入探讨RocketMQ的事务消息机制,通过六个步骤解析事务消息发送的完整流程,包括生产者发送半事务消息、Broker持久化、本地事务执行、二次确认、消息回查及处理,揭示RocketMQ如何确保全局事务一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

以电商交易场景为例,用户支付订单这一核心操作的同时会涉及到下游物流发货、积分变更、购物车状态清空等多个子系统的变更。

普通消息无法像数据库事务一样具备提交、回滚和统一协调的能力。而 RocketMQ 的分布式事务消息是在普通消息的基础上,将二阶段提交与本地事务绑定,实现全局提交结果的一致性

请添加图片描述

事务消息发送过程概述

请添加图片描述

事务消息发送步骤如下:

1、生产者将半事务消息发送到 RocketMQ Broker。

2、RocketMQ Broker 将消息持久化成功之后,向生产者返回 ACK 确认消息已经发送成功,此时消息不会投递给消费者。半事务消息更换主题、队列ID等信息,然后将原来的主题、队列ID等信息作为属性进行存储。

3、生产者开始执行本地事务,并根据本地事务的执行结果向服务端提交二次确认结果(Commit、Rollback)。

4、服务端收到确认结果后处理逻辑如下:

  • 二次确认结果为 Commit:基于半事务消息的相关属性以及它原来的主题、队列ID来创建新的事务消息并进行持久化,如果持久化成功则可以投递给消费者,最后丢弃半事务消息。
  • 二次确认结果为 Rollback:丢弃半事务消息。

半事务消息的丢弃,其实是将半事务消息移动到一个新的主题以及OP队列中,而非直接物理删除。

5、服务端每隔60秒进行一次检查,如果发现一直(6秒内)没有收到生产者提交的二次确认结果,或者服务端收到的二次确认结果为 Unknown 未知状态,则服务端对生产者发起消息回查。如果服务端发现原始生产者已经崩溃,则会向同一生产者组的其它生产者实例发起消息回查。

6、生产者收到消息回查后,检查本地事务执行的最终状态并再次提交二次确认结果。

源码分析-步骤一

生产者将半事务消息发送到 RocketMQ Broker。

TransactionMQProducer

首先看下 TransactionMQProducer 的 sendMessageInTransaction 方法的处理逻辑。

@Override
public TransactionSendResult sendMessageInTransaction(final Message msg,
    final Object arg) throws MQClientException {
   
   
  	// 校验事务监听器是否存在
    if (null == this.transactionListener) {
   
   
        throw new MQClientException("TransactionListener is null", null);
    }
	// 交给 DefaultMQProducerImpl 继续处理
    return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg);
}

DefaultMQProducerImpl

接下来看下 DefaultMQProducerImpl 的 sendMessageInTransaction 方法的处理逻辑。

public TransactionSendResult sendMessageInTransaction(final Message msg,
                                                      final LocalTransactionExecuter localTransactionExecuter, final Object arg)
    throws MQClientException {
   
   
    TransactionListener transactionListener = getCheckListener();
  	// 校验本地事务执行器、事务监听器是否同时为空(本地事务执行器将会在 5.0.0 版本移除,推荐使用事务监听器)
    if (null == localTransactionExecuter && null == transactionListener) {
   
   
        throw new MQClientException("tranExecutor is null", null);
    }
  	// 校验主题、消息体是否符合要求
    Validators.checkMessage(msg, this.defaultMQProducer);

    SendResult sendResult = null;
  	// 对消息添加TRAN_MSG属性(属性值为true),也就是标记为事务消息
    MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
  	// 对消息添加PGROUP属性(属性值为生产者组)
    MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
    try {
   
   
      	// 1、发送半事务消息
        sendResult = this.send(msg);
    } catch (Exception e) {
   
   
        throw new MQClientException("send message Exception", e);
    }

    LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
    Throwable localException = null;
  	// 判断消息的发送状态
    switch (sendResult.getSendStatus()) {
   
   
        case SEND_OK: {
   
   
            try {
   
   
                if (sendResult.getTransactionId() != null) {
   
   
                  	// 对消息添加"__transactionId__"属性
                    msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
                }
              	// 获取消息的UNIQ_KEY属性对应的属性值
                String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
                if (null != transactionId && !"".equals(transactionId)) {
   
   
                  	// 设置事务id
                    msg.setTransactionId(transactionId);
                }
                if (null != localTransactionExecuter) {
   
   
                    localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
                } else if (transactionListener != null) {
   
   
                    log.debug("Used new transaction API");
                  	// 2、执行本地事务
                    localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
                }
                if (null == localTransactionState) {
   
   
                    localTransactionState = LocalTransactionState.UNKNOW;
                }

                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:
            localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
            break;
        default:
            break;
    }

    try {
   
   
      	// 3、生产者根据本地事务的执行结果向服务端提交二次确认结果
        this.endTransaction(sendResult, localTransactionState, localException);
    } catch (Exception e) {
   
   
        log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
    }
  	// 组装TransactionSendResult实例并返回
    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.1 send 方法

接下来看下 send 方法的处理逻辑。

public SendResult send(
    Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
   
   
  	// sendMsgTimeout:默认3000,即发送消息的超时时间
    return send(msg, this.defaultMQProducer.getSendMsgTimeout());
}

public SendResult send(Message msg,
    long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
   
   
  	// 事务消息默认采用同步的方式
    return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
}
private SendResult sendDefaultImpl(
    Message msg,
    final CommunicationMode communicationMode,
    final SendCallback sendCallback,
    final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
   
   
    // 校验生产者的状态
  	this.makeSureStateOK();
  	// 校验主题、消息体是否符合要求
    Validators.checkMessage(msg, this.defaultMQProducer);

    final long invokeID = random.nextLong();
    long beginTimestampFirst = System.currentTimeMillis();
    long beginTimestampPrev = beginTimestampFirst;
    long endTimestamp = beginTimestampFirst;
    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
  	if (topicPublishInfo != null && topicPublishInfo.ok()) {
   
   
        boolean callTimeout = false;
        MessageQueue mq = null;
        Exception exception = null;
        SendResult sendResult = null;
      	// 计算最多可以发送消息的次数
      	// retryTimesWhenSendFailed 参数用于发送失败的重试次数,默认 2
        int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
        int times = 0;
        String[] brokersSent = new String[timesTotal];
        for (; times < timesTotal; times++) {
   
   
            String lastBrokerName = null == mq ? null : mq.getBrokerName();
          	// 从消息队列列表中选取一个消息队列
            MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
            if (mqSelected != null) {
   
   
                mq = mqSelected;
                brokersSent[times] = mq.getBrokerName();
                try {
   
   
                    beginTimestampPrev = System.currentTimeMillis();
                    long costTime = beginTimestampPrev - beginTimestampFirst;
                  	// 如果调用超时,则不再进行重试
                    if (timeout < costTime) {
   
   
                        callTimeout = true;
                        break;
                    }
					// 调用sendKernelImpl方法继续处理
                    sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                    endTimestamp = System.currentTimeMillis();
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                    switch (communicationMode) {
   
   
                        case ASYNC:
                            return null;
                        case ONEWAY:
                            return null;
                        case SYNC:
                            if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
   
   
                              	// 获取 retryAnotherBrokerWhenNotStoreOK 参数值,表示发送失败时是否重试其它的broker,默认false
                                if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
   
   
                                    continue;
                                }
                            }

                            return sendResult;
                        default:
                            break;
                    }
                } catch (RemotingException e) {
   
   
                    endTimestamp = System.currentTimeMillis();
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                    log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                    log.warn(msg.toString());
                    exception = e;
                    continue;
                } catch (MQClientException e) {
   
   
                    endTimestamp = System.currentTimeMillis();
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                    log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                    log.warn(msg.toString());
                    exception = e;
                    continue;
                } catch (MQBrokerException e) {
   
   
                    endTimestamp = System.currentTimeMillis();
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                    log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                    log.warn(msg.toString());
                    exception = e;
                    switch (e.getResponseCode()) {
   
   
                        case ResponseCode.TOPIC_NOT_EXIST:
                        case ResponseCode.SERVICE_NOT_AVAILABLE:
                        case ResponseCode.SYSTEM_ERROR:
                        case ResponseCode.NO_PERMISSION:
                        case ResponseCode.NO_BUYER_ID:
                        case ResponseCode.NOT_IN_CURRENT_UNIT:
                            continue;
                        default:
                            if (sendResult != null) {
   
   
                                return sendResult;
                            }

                            throw e;
                    }
                } catch (InterruptedException e) {
   
   
                    endTimestamp = System.currentTimeMillis();
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                    log.warn(String.format("sendKernelImpl exception, throw exception, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                    log.warn(msg.toString());

                    log.warn("sendKernelImpl exception", e);
                    log.warn(msg.toString());
                    throw e;
                }
            } else {
   
   
                break;
            }
        }

        if (sendResult != null) {
   
   
            return sendResult;
        }

        String info = String.format("Send [%d] times, still failed, cost [%d]ms, Topic: %s, BrokersSent: %s",
            times,
            System.currentTimeMillis() - beginTimestampFirst,
            msg.getTopic(),
            Arrays.toString(brokersSent));

        info += FAQUrl.suggestTodo(FAQUrl.SEND_MSG_FAILED);

        MQClientException mqClientException = new MQClientException(info, exception);
        if (callTimeout) {
   
   
            throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
        }

        if (exception instanceof MQBrokerException) {
   
   
            mqClientException.setResponseCode(((MQBrokerException) exception).getResponseCode());
        } else if (exception instanceof RemotingConnectException) {
   
   
            mqClientException.setResponseCode(ClientErrorCode.CONNECT_BROKER_EXCEPTION);
        } else if (exception instanceof RemotingTimeoutException) {
   
   
            mqClientException.setResponseCode(ClientErrorCode.ACCESS_BROKER_TIMEOUT);
        } else if (exception instanceof MQClientException) {
   
   
            mqClientException.setResponseCode(ClientErrorCode.BROKER_NOT_EXIST_EXCEPTION);
        }

        throw mqClientException;
    }
	// 校验 NameServer 地址列表是否为空,如果为空,则抛出异常
    List<String> nsList = this.getmQClientFactory().getMQClientAPIImpl().getNameServerAddressList();
    if (null == nsList || nsList.isEmpty()) {
   
   
        throw new MQClientException(
            "No name server address, please set it." + FAQUrl.suggestTodo(FAQUrl.NAME_SERVER_ADDR_NOT_EXIST_URL), null).setResponseCode(ClientErrorCode.NO_NAME_SERVER_EXCEPTION);
    }

    throw new MQClientException("No route info of this topic, " + msg.getTopic() + FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO),
        null).setResponseCode(ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION);
}

接着看 sendKernelImpl 方法的处理。

private SendResult sendKernelImpl(final Message msg,
                                  final MessageQueue mq,
                                  final CommunicationMode communicationMode,
                                  final SendCallback sendCallback,
                                  final TopicPublishInfo topicPublishInfo,
                                  final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
   
   
    long beginStartTime = System.currentTimeMillis();
  	// 从缓存中获取broker地址
    String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
    if (null == brokerAddr) {
   
   
        tryToFindTopicPublishInfo(mq.getTopic());
        brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
    }

    SendMessageContext context = null;
    if (brokerAddr != null) {
   
   
      	// vipChannelEnabled 参数用于判断是否开启vip通道,默认 false
        brokerAddr = MixAll.brokerVIPChannel(this.defaultMQProducer.isSendMessageWithVIPChannel(), brokerAddr);

        byte[] prevBody = msg.getBody();
        try {
   
   
          	// 如果消息不是批次消息
            if (!(msg instanceof MessageBatch)) {
   
   
              	// 对消息添加一个 UNIQ_KEY 属性
                MessageClientIDSetter.setUniqID(msg);
            }

            int sysFlag = 0;
            boolean msgBodyCompressed = false;
          	// 如果单条消息的消息体的大小超过了4KB(由 compressMsgBodyOverHowmuch 参数控制当消息大小达到多少时可以进行压缩,默认4096),则对消息体进行压缩
            if (this.tryToCompressMessage(msg)) {
   
   
                sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
              	// 标记消息已压缩
                msgBodyCompressed = true;
            }

          	// 获取消息的"TRAN_MSG"属性值
            final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
            if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
   
   
                sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
            }
						
          	// 如果CheckForbiddenHook列表非空
            if (hasCheckForbiddenHook(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值