RocketMQ 分布式事务消息的源码级研究

·  阅读 369
RocketMQ 分布式事务消息的源码级研究

白菜Java自习室 涵盖核心知识

1. 事务消息原理

RocketMQ 提供了事务消息的功能,采用 2PC(两段式协议)+补偿机制(事务回查) 的分布式事务功能,通过消息队列 RocketMQ 版事务消息能达到分布式事务的最终一致。

RocketMQ 相关的前置知识建议阅读作者的文章: Java工程师的进阶之路 RocketMQ篇

1.1. 半事务消息

暂不能投递的消息,发送方已经成功地将消息发送到了消息队列 RocketMQ 版服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。

1.2. 消息回查

由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列 RocketMQ 版服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit 或是 Rollback),该询问过程即消息回查。

1.3. 交互流程

总体而言 RocketMQ 事务消息分为两条主线:

  1. 发送流程:发送 half message(半消息),执行本地事务,发送事务执行结果;
  2. 回查流程:MQ 定时任务扫描半消息,回查本地事务,发送事务执行结果;

5d502f11839a41478df2582038f2ded7_tplv-k3u1fbpfcp-zoom-1.png

事务消息发送步骤如下

  1. 发送方将半事务消息发送至消息队列 RocketMQ 版服务端。
  2. 消息队列 RocketMQ 版服务端将消息持久化成功之后,向发送方返回 Ack 确认消息已经发送成功,此时消息为半事务消息。
  3. 发送方开始执行本地事务逻辑。
  4. 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息。

事务消息回查步骤如下

  1. 在断网或者是应用重启的特殊情况下,上述步骤 4 提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查。
  2. 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  3. 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半事务消息进行操作。

2. 事务消息源码分析

代码版本:

        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.0.4</version>
        </dependency>
复制代码

2.1. Producer 端的处理过程

Producer 根据事务消息发送流程,可以分析得到大致有三个动作:

  1. 发送事务半消息(prepare);
  2. 执行本地事务;
  3. 提交/回滚事务(commit/rollback);

发送事务半消息

在本地应用发送事务消息的核心类是 TransactionMQProducer,该类通过继承 DefaultMQProducer 来复用大部分发送消息相关的逻辑,这个类的 sendMessageTransaction() 方法发送事务消息。

public class TransactionMQProducer extends DefaultMQProducer {

    // ...

    public TransactionSendResult sendMessageInTransaction(Message msg, Object arg) throws MQClientException {
        // 判断transactionListener是否存在
        if (null == this.transactionListener) {
            throw new MQClientException("TransactionListener is null", (Throwable)null);
        } else {
            // 发送事务消息
            msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic()));
            return this.defaultMQProducerImpl.sendMessageInTransaction(msg, (LocalTransactionExecuter)null, arg);
        }
    }
    
}
复制代码

这里的 transactionListener 就是上面所说的消息回查的类,它提供了2个方法:

  1. 执行本地事务: executeLocalTransaction(final Message msg, final Object arg);
  2. 回查本地事务: checkLocalTransaction(final MessageExt msg);

接着看 DefaultMQProducerImpl.sendMessageInTransaction() 方法:

public class DefaultMQProducerImpl implements MQProducerInner {

    // ...
    
    public TransactionSendResult sendMessageInTransaction(final Message msg, 
                final LocalTransactionExecuter localTransactionExecuter, final Object arg)
        throws MQClientException {
        // 判断检查本地事务的listenner是否存在
        TransactionListener transactionListener = getCheckListener();
        if (null == localTransactionExecuter && null == transactionListener) {
            throw new MQClientException("tranExecutor is null", null);
        }
        Validators.checkMessage(msg, this.defaultMQProducer);

        SendResult sendResult = null;
        // msg设置参数TRAN_MSG,表示为事务消息
        MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
        MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
        try {
            // 发送消息
            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) {
                        msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
                    }
                    String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
                    if (null != transactionId && !"".equals(transactionId)) {
                        msg.setTransactionId(transactionId);
                    }
                    if (null != localTransactionExecuter) {
                        localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
                    } else if (transactionListener != null) {
                        log.debug("Used new transaction API");
                        // 发送消息成功,执行本地事务
                        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 {
            // 执行endTransaction方法,
            // 如果半消息发送失败或本地事务执行失败告诉服务端是删除半消息,
            // 半消息发送成功且本地事务执行成功则告诉服务端生效半消息
            this.endTransaction(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;
    }
    
}
复制代码

sendMessageInTransaction() 方法主要做了以下事情:

  1. 给消息打上事务消息相关的 tag,用于 broker 区分普通消息和事务消息;
  2. 发送半消息 (half message);
  3. 发送成功则由 transactionListener 执行本地事务;
  4. 执行 endTransaction() 方法,告诉 broker 执行 commit/rollback。

执行本地事务

接着我们回到 上面 Producer 发送半消息的地方,往下继续看:

       switch (sendResult.getSendStatus()) {
            case SEND_OK: {
                try {
                    if (sendResult.getTransactionId() != null) {
                        msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
                    }
                    String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
                    if (null != transactionId && !"".equals(transactionId)) {
                        msg.setTransactionId(transactionId);
                    }
                    if (null != localTransactionExecuter) {
                        localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
                    } else if (transactionListener != null) {
                        log.debug("Used new transaction API");
                        // 发送消息成功,执行本地事务
                        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;
        }
复制代码

事务半消息发送成功后,会调用 transactionListener.executeLocalTransaction() 方法执行本地事务。只有半消息发送成功后,才会执行本地事务,如果半消息发送失败,则设置回滚。

结束事务(commit/rollback)

本地事务执行后,则调用 this.endTransaction() 方法,根据本地事务执行状态,去提交事务或者回滚事务。 如果半消息发送失败或本地事务执行失败告诉服务端是删除半消息,半消息发送成功且本地事务执行成功则告诉服务端生效半消息。

public class DefaultMQProducerImpl implements MQProducerInner {

    // ...
    
    public void endTransaction(final SendResult sendResult, 
                final LocalTransactionState localTransactionState, final Throwable localException) throws RemotingException, MQBrokerException, InterruptedException, UnknownHostException {
        final MessageId id;
        if (sendResult.getOffsetMsgId() != null) {
            id = MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId());
        } else {
            id = MessageDecoder.decodeMessageId(sendResult.getMsgId());
        }
        String transactionId = sendResult.getTransactionId();
        final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName());
        EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader();
        requestHeader.setTransactionId(transactionId);
        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;
        }

        requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
        requestHeader.setTranStateTableOffset(sendResult.getQueueOffset());
        requestHeader.setMsgId(sendResult.getMsgId());
        String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null;
        this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark,
            this.defaultMQProducer.getSendMsgTimeout());
    }
    
}
复制代码

2.2. Broker 端的处理过程

Broker 根据事务消息处理流程,可以分析得到大致有三个动作:

  1. 判断消息类型,进行消息存储;
  2. 半消息事务回查;
  3. 处理事务消息提交、回滚命令;

判断消息类型,进行消息存储

Broker 端通过 SendMessageProcessor.processRequest() 方法接收处理 Producer 发送的消息 最后会调用到 SendMessageProcessor.sendMessage(),判断消息类型,进行消息存储。

public class SendMessageProcessor extends AbstractSendMessageProcessor implements NettyRequestProcessor {

    // ...
    
    private RemotingCommand sendMessage(final ChannelHandlerContext ctx,
                                        final RemotingCommand request,
                                        final SendMessageContext sendMessageContext,
                                        final SendMessageRequestHeader requestHeader) throws RemotingCommandException {

        final RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class);
        final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader)response.readCustomHeader();

        response.setOpaque(request.getOpaque());

        response.addExtField(MessageConst.PROPERTY_MSG_REGION, this.brokerController.getBrokerConfig().getRegionId());
        response.addExtField(MessageConst.PROPERTY_TRACE_SWITCH, String.valueOf(this.brokerController.getBrokerConfig().isTraceOn()));

        log.debug("receive SendMessage request command, {}", request);

        final long startTimstamp = this.brokerController.getBrokerConfig().getStartAcceptSendRequestTimeStamp();
        if (this.brokerController.getMessageStore().now() < startTimstamp) {
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark(String.format("broker unable to service, until %s", UtilAll.timeMillisToHumanString2(startTimstamp)));
            return response;
        }

        response.setCode(-1);
        super.msgCheck(ctx, requestHeader, response);
        if (response.getCode() != -1) {
            return response;
        }

        final byte[] body = request.getBody();

        int queueIdInt = requestHeader.getQueueId();
        TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic());

        if (queueIdInt < 0) {
            queueIdInt = Math.abs(this.random.nextInt() % 99999999) % topicConfig.getWriteQueueNums();
        }

        MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
        msgInner.setTopic(requestHeader.getTopic());
        msgInner.setQueueId(queueIdInt);

        if (!handleRetryAndDLQ(requestHeader, response, request, msgInner, topicConfig)) {
            return response;
        }

        msgInner.setBody(body);
        msgInner.setFlag(requestHeader.getFlag());
        MessageAccessor.setProperties(msgInner, MessageDecoder.string2messageProperties(requestHeader.getProperties()));
        msgInner.setBornTimestamp(requestHeader.getBornTimestamp());
        msgInner.setBornHost(ctx.channel().remoteAddress());
        msgInner.setStoreHost(this.getStoreHost());
        msgInner.setReconsumeTimes(requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes());
        String clusterName = this.brokerController.getBrokerConfig().getBrokerClusterName();
        MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_CLUSTER, clusterName);
        msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
        PutMessageResult putMessageResult = null;
        Map<String, String> oriProps = MessageDecoder.string2messageProperties(requestHeader.getProperties());
        String traFlag = oriProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
        if (traFlag != null && Boolean.parseBoolean(traFlag)
            && !(msgInner.getReconsumeTimes() > 0 && msgInner.getDelayTimeLevel() > 0)) { //For client under version 4.6.1
            if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) {
                response.setCode(ResponseCode.NO_PERMISSION);
                response.setRemark(
                    "the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
                        + "] sending transaction message is forbidden");
                return response;
            }
            // 存储事务消息
            putMessageResult = this.brokerController.getTransactionalMessageService().prepareMessage(msgInner);
        } else {
            // 存储普通消息
            putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
        }

        return handlePutMessageResult(putMessageResult, response, request, msgInner, responseHeader, sendMessageContext, ctx, queueIdInt);

    }
    
}
复制代码

接着看存储半消息的代码 prepareMessage(msgInner) :

public class TransactionalMessageBridge {

    // ...
    
    // 存储事务半消息
    public PutMessageResult putHalfMessage(MessageExtBrokerInner messageInner) {
        return store.putMessage(parseHalfMessageInner(messageInner));
    }

    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;
    }
}
复制代码

备份消息的原主题名称与原队列ID,然后取消事务消息的消息标签,重新设置消息的主题为:RMQ_SYS_TRANS_HALF_TOPIC,队列ID固定为0。与其他普通消息区分开,然后完成消息持久化。

到这里,Broker 就初步处理完了 Producer 发送的事务半消息。

半消息事务回查

两段式协议发送与提交回滚消息,执行完本地事务消息的状态为 UNKNOW 时,结束事务不做任何操作。通过事务状态定时回查得到发送端的事务状态是 rollback() 或 commit() 。 通过 TransactionalMessageCheckService 线程定时去检测 RMQ_SYS_TRANS_HALF_TOPIC 主题中的消息,回查消息的事务状态。

  • RMQ_SYS_TRANS_HALF_TOPIC: prepare 消息的主题,事务消息首先先进入到该主题。
  • RMQ_SYS_TRANS_OP_HALF_TOPIC: 当消息服务器收到事务消息的提交或回滚请求后,会将消息存储在该主题下。

e88f28ca7204427aa00611f36a354a56_tplv-k3u1fbpfcp-zoom-1.png

HALP 和 OP 主题队列都只有一个 ID 为 0,OP 队列存储的是 HALF 中事务消息已经 commit 或者 rollback 的事务消息对应的 offset(队列我们可以认为是一个数组,存储的就是这个队列的下标),因为异步的事务消息的状态和事务消息的顺序是不一致的

代码入口如下:

public class TransactionalMessageCheckService extends ServiceThread {
    
    // ...
    
    @Override
    public void run() {
        log.info("Start transaction check service thread!");
        // 执行间隔
        long checkInterval = brokerController.getBrokerConfig().getTransactionCheckInterval();
        while (!this.isStopped()) {
            this.waitForRunning(checkInterval);
        }
        log.info("End transaction check service thread!");
    }

    @Override
    protected void onWaitEnd() {
        // 事务过期时间
        long timeout = brokerController.getBrokerConfig().getTransactionTimeOut();
        int checkMax = brokerController.getBrokerConfig().getTransactionCheckMax();
        long begin = System.currentTimeMillis();
        log.info("Begin to check prepare message, begin time:{}", begin);
        // 检查本地事务
        this.brokerController.getTransactionalMessageService().check(timeout, checkMax, this.brokerController.getTransactionalMessageCheckListener());
        log.info("End to check prepare message, consumed time:{}", System.currentTimeMillis() - begin);
    }
    
}
复制代码

大致流程如下:

477cf3d3561044c881f3e723ed4eff00_tplv-k3u1fbpfcp-zoom-1.png

处理事务消息提交、回滚命令

当 Producer 或者回查定时任务提交/回滚事务的时候,Broker 如何处理事务消息提交、回滚命令的。

public class EndTransactionProcessor extends AsyncNettyRequestProcessor implements NettyRequestProcessor {
    
    // ...
    
    @Override
    public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws
        RemotingCommandException {
        final RemotingCommand response = RemotingCommand.createResponseCommand(null);
        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;
        }

        if (requestHeader.getFromTransactionCheck()) {
            switch (requestHeader.getCommitOrRollback()) {
                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;
                }
                // 提交事务
                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;
                }
                // 回滚处理
                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()) {
                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;
                }

                case MessageSysFlag.TRANSACTION_COMMIT_TYPE: {
                    break;
                }

                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;
            }
        }
        OperationResult result = new OperationResult();
        // 提交事务
        if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
            // 根据commitLogOffset从commitlog文件中查找消息
            result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
            if (result.getResponseCode() == ResponseCode.SUCCESS) {
                // 字段检查
                RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
                if (res.getCode() == ResponseCode.SUCCESS) {
                    // 恢复事务消息的真实的主题、队列,并设置事务ID
                    MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
                    // 设置消息的相关属性,取消事务相关的系统标记
                    msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback()));
                    msgInner.setQueueOffset(requestHeader.getTranStateTableOffset());
                    msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());
                    msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());
                    MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED);
                    // 发送最终消息,存储,被consumer消费
                    RemotingCommand sendResult = sendFinalMessage(msgInner);
                    if (sendResult.getCode() == ResponseCode.SUCCESS) {
                        // 删除预处理消息(prepare)
                        // 其实是将消息存储在主题为:RMQ_SYS_TRANS_OP_HALF_TOPIC的主题中,代表这些消息已经被处理(提交或回滚)                      
                        this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
                    }
                    return sendResult;
                }
                return res;
            }
        } else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
            // 根据commitlogOffset查找消息
            result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);
            if (result.getResponseCode() == ResponseCode.SUCCESS) {
                // 字段检查
                RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
                if (res.getCode() == ResponseCode.SUCCESS) {
                    // 删除预处理消息(prepare)
                    // 将消息存储在RMQ_SYS_TRANS_OP_HALF_TOPIC中,代表该消息已被处理
                    this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
                }
                return res;
            }
        }
        response.setCode(result.getResponseCode());
        response.setRemark(result.getResponseRemark());
        return response;
    }
    
}
复制代码

这里的逻辑很清晰,其核心实现如下:

  1. 根据 commitlogOffset 找到消息;
  2. 如果是提交动作,就恢复原消息的主题与队列,再次存入 commitlog 文件进而转到消息消费队列,供消费者消费,然后将原预处理消息存入一个新的主题 RMQ_SYS_TRANS_OP_HALF_TOPIC,代表该消息已被处理;
  3. 回滚消息,则直接将原预处理消息存入一个新的主题 RMQ_SYS_TRANS_OP_HALF_TOPIC,代表该消息已被处理。

整体实现流程:

129fd3c8ee374a45976d96d1accfe844_tplv-k3u1fbpfcp-zoom-1.png

3. 异常情况处理

假如我们有个订单系统,下单后要调用优惠券系统,我们采用 RocketMq 的方式,在下单支付成功后发送消息给优惠券系统派发优惠券,这里通过事务消息的方式,保证一定可以派发优惠券成功。 我们来思考下几种异常场景,看看 RocketMq 能不能解决。

1. Producer 发送半消息失败怎么办 ?

可能由于网络或者 MQ 故障,导致 Producer 订单系统 发送半消息 (prepare) 失败。

解决办法:这时订单系统可以执行回滚操作,比如“订单关闭”等,走逆向流程退款给用户。

2. 半消息发送成功,本地事务执行失败怎么办 ?

如果订单系统发送的半消息成功了,但是执行本地事务失败了,如更新订单状态为 “已支付” 失败。

解决办法:这种情况下,执行本地事务失败后,会返回 rollback 给 MQ,MQ 会删除之前发送的半消息。 也就不会调用优惠券系统了。

3. 半消息发送成功,没收到 MQ 返回的响应怎么办 ?

假如订单系统发送半消息成功后,没有收到 MQ 返回的响应。

解决办法:这个时候可能是因为网络问题,或者其他异常报错,订单系统误以为发送 MQ 半消息失败,执行了逆向回滚流程。但这个时候其实 MQ 已经保存半消息成功了,那这个消息怎么处理?这个时候 MQ 的后台消息回查定时任务 TransactionalMessageCheckService 会每隔1分钟扫描一次半消息队列,判断是否需要消息回查,然后回查订单系统的本地事务,这时 MQ 就会发现订单已经变成“已关闭”,此时就要发送 rollback 请求给 MQ,删除之前的半消息。

4. 如果发送 commit/rollback 命令失败了怎么办 ?

订单系统发送的半消息成功了,执行本地事务也成功/失败了,MQ 去发送 commit/rollback 命令失败。

解决办法:这个其实也是通过定时任务 TransactionalMessageCheckService,它会发现这个消息超过一定时间还没有进行二阶段处理,就会回查本地事务。

5. 如果发送 commit 确认之后优惠券派发失败了怎么办 ?

订单系统发送的半消息成功后,执行本地事务也成功了,MQ 成功发送 commit 命令到优惠券系统,但是优惠券派发失败。

解决办法:优惠券本地事务执行失败,就对该条消息返回重试状态,重新消费此消息(一般需要设置重试次数),直到成功。如果永远会执行失败呢?这种情况可以把执行记录落库,用补偿机制如定时任务进行处理,或者人工干预。

分类:
后端
标签:
分类:
后端
标签: