RocketMQ源码分析 consumer消费,并发、顺序、延时、事务消息总结

1.消费客户端启动流程

先贴下consume client启动的流程图

 消费端启动和producer启动很类似,可以和producer启动进行对比。

不同之处是消费端的PullMessageService、RebalanceService才有真正作用,而producer该两个服务线程是无用的,这两个服务线程也是消费端的核心。

2.消费队列负载均衡RebalanceService

先贴总体流程图

消费端消息队列负载的核心功能方法是org.apache.rocketmq.client.impl.consumer.RebalanceImpl.updateProcessQueueTableInRebalance(String, Set<MessageQueue>, boolean),只解释该方法,其余方法看流程图看代码就很容易明白。

传入参数Set<MessageQueue>是经过负载后分配给当前消费端的mq集合,boolean表示是顺序消费true,并发消费false。

看代码注释参考

 /*
 * 消费端重新负载的核心方法
 * 传入参数:mqSet即分配给该消费者的队列, isOrder为false表示非顺序消息
 * 功能就是更新处理器队列集合RebalanceImpl.processQueueTable
 */
private boolean updateProcessQueueTableInRebalance(final String topic, final Set<MessageQueue> mqSet,
    final boolean isOrder) {
    boolean changed = false;

    Iterator<Entry<MessageQueue, ProcessQueue>> it = this.processQueueTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<MessageQueue, ProcessQueue> next = it.next();
        MessageQueue mq = next.getKey();
        ProcessQueue pq = next.getValue();

        if (mq.getTopic().equals(topic)) {
            if (!mqSet.contains(mq)) {//比如减少or新增了消费端,分配给当前消费端的MessageQueue变化了了,那么可能原来的MessageQueue就不在当前重新负载后的mqSet
                pq.setDropped(true);//丢弃该ProcessQueue,那么在拉取消费的时候就不会该ProcessQueue进行处理
                if (this.removeUnnecessaryMessageQueue(mq, pq)) {//把该mq的客户端消费offset更新到broker保存,移除客户端该mq的消费offset记录,如果是顺序消费则到broker解锁mq
                    it.remove();//移除
                    changed = true;
                    log.info("doRebalance, {}, remove unnecessary mq, {}", consumerGroup, mq);
                }
            } else if (pq.isPullExpired()) {//拉取失效,
                switch (this.consumeType()) {
                    case CONSUME_ACTIVELY:
                        break;
                    case CONSUME_PASSIVELY://push走这里
                        pq.setDropped(true);//把ProcessQueue置为失效,这样在PullService线程拉取的时候该对象是失效状态,就不再拉取该对象
                        if (this.removeUnnecessaryMessageQueue(mq, pq)) {
                            it.remove();
                            changed = true;
                            log.error("[BUG]doRebalance, {}, remove unnecessary mq, {}, because pull is pause, so try to fixed it",
                                consumerGroup, mq);
                        }
                        break;
                    default:
                        break;
                }
            }
        }
    }//end while

    List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
    for (MessageQueue mq : mqSet) {
        if (!this.processQueueTable.containsKey(mq)) {//说明该mq是本次负载新增的
            if (isOrder && !this.lock(mq)) {//顺序消费到broker加锁该MessageQueue
                log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
                continue;
            }

            this.removeDirtyOffset(mq);//消费客户端移除该mq的消费offset
            ProcessQueue pq = new ProcessQueue();
            long nextOffset = this.computePullFromWhere(mq);//向broker发送命令QUERY_CONSUMER_OFFSET获取broker端记录的该mq的消费offset
            if (nextOffset >= 0) {
                ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
                if (pre != null) {
                    log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
                } else {//不存在
                    log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
                    PullRequest pullRequest = new PullRequest();
                    pullRequest.setConsumerGroup(consumerGroup);
                    pullRequest.setNextOffset(nextOffset);//该messagequeue在broker端的消费位置
                    pullRequest.setMessageQueue(mq);
                    pullRequest.setProcessQueue(pq);
                    pullRequestList.add(pullRequest);
                    changed = true;
                }
            } else {
                log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
            }
        }
    }

    this.dispatchPullRequest(pullRequestList);//遍历pullRequestList集合,吧pullRequest对象添加到PullMessageService服务线程的阻塞队列内供PullMessageService拉取执行

    return changed;
}

3.消费拉取PullMessageService流程

消息拉取是异步方式,总共涉及到三个回调

第一个回调:netty io通过网络把数据发送出去,即发送成功,执行netty io的监听器NettyRemotingAbstract$4,发送成功设置ResponseFuture.sendRequestOK=true,发送失败,则把ResponseFuture从NettyRemotingAbstract.responseTable集合移除。

第二个回调:InvokeCallback的回调,即MQClientAPIImpl$2,该操作是在pull到消息or超时由扫描发起,入口是ResponseFuture.executeInvokeCallback(),继而执行MQClientAPIImpl$2.operationComplete(ResponseFuture),目的就是为了执行PullCallback

第三个回调:PullCallback回调,执行拉取消息成功后的回调,DefaultMQPushConsumerImpl$1.onSuccess(PullResult pullResult),或者执行异常回调DefaultMQPushConsumerImpl$1.onException(Throwable e)

代码和注释如下

PullCallback pullCallback = new PullCallback() {//DefaultMQPushConsumerImpl$1
    	/*
    	 * 功能就是把拉取到的消息保存到processqueue上,然后进行客户端实际业务消费,最后把pullRequest重新添加到阻塞队列供pullmessageservice服务线程重新拉取
    	 */
        @Override
        public void onSuccess(PullResult pullResult) {
            if (pullResult != null) {
                pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
                    subscriptionData);//解码拉取到的消息,填充pullResult对象,把解码的消息保存到PullResult.msgFoundList

                switch (pullResult.getPullStatus()) {
                    case FOUND://消息拉取结果,消息拉取到了
                        long prevRequestOffset = pullRequest.getNextOffset();//拉取到的消息的位置,相对于consumer queue
                        pullRequest.setNextOffset(pullResult.getNextBeginOffset());//下次待拉取的消息在consumer queue的位置
                        long pullRT = System.currentTimeMillis() - beginTimestamp;
                        DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
                            pullRequest.getMessageQueue().getTopic(), pullRT);//统计拉取消息的responsetime

                        long firstMsgOffset = Long.MAX_VALUE;
                        if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {//msgFoundList为空说明没有拉取到消息
                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);//没拉取到消息的情况下,把pullRequest重新放入到pullservice的队列再次拉取
                        } else {
                            firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();//拉取到的消息中的第一个消息在commitlog的位置

                            DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
                                pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());//统计tps

														//把拉取到的32条消息保存到ProcessQueue.msgTreeMap
                            boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
                            DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
                                pullResult.getMsgFoundList(),
                                processQueue,
                                pullRequest.getMessageQueue(),
                                dispatchToConsume);//客户端消费并发执行 ConsumeRequest.run()

                            //把PullRequest重新保存到PullMessageService.pullRequestQueue阻塞队列,供消费线程继续执行消息拉取
                            if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
                                DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
                                    DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
                            } else {
                                DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);//把PullRequest重新保存到PullMessageService.pullRequestQueue阻塞队列,供消费线程继续执行消息拉取
                            }
                        }

                        if (pullResult.getNextBeginOffset() < prevRequestOffset
                            || firstMsgOffset < prevRequestOffset) {
                            log.warn(
                                "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
                                pullResult.getNextBeginOffset(),
                                firstMsgOffset,
                                prevRequestOffset);
                        }

                        break;
                    case NO_NEW_MSG://未拉取到消息
                        pullRequest.setNextOffset(pullResult.getNextBeginOffset());//拉取下一个新的offset

                        DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);//本次拉取到的消息总size==0,则更新消费端本地的offset

                        DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);//把pullRequest重新保存到pullmessageservice的阻塞队列供拉取线程重新执行
                        break;
                    case NO_MATCHED_MSG://消息拉取到了但是不匹配tag,broker进行tag过滤
                        pullRequest.setNextOffset(pullResult.getNextBeginOffset());//拉取下一个新的offset

                        DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);

                        DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);//把pullRequest重新保存到pullmessageservice的阻塞队列供拉取线程重新执行
                        break;
                    case OFFSET_ILLEGAL://offset非法,那么该pullRequest不会被重新进行拉取
                        log.warn("the pull request offset illegal, {} {}",
                            pullRequest.toString(), pullResult.toString());
                        pullRequest.setNextOffset(pullResult.getNextBeginOffset());//拉取下一个新的offset

                        pullRequest.getProcessQueue().setDropped(true);//抛弃processqueue
                        DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {

                            @Override
                            public void run() {
                                try {
                                    DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
                                        pullRequest.getNextOffset(), false);//更新消费端本地的offset到RemoteBrokerOffsetStore.offsetTable

                                    DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());//把当前mq的消费offset更新保存到broker

                                    DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());//从处理队列集合移除该processqueue

                                    log.warn("fix the pull request offset, {}", pullRequest);
                                } catch (Throwable e) {
                                    log.error("executeTaskLater Exception", e);
                                }
                            }
                        }, 10000);
                        break;
                    default:
                        break;
                }
            }
        }

        @Override
        public void onException(Throwable e) {
            if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                log.warn("execute the pull request exception", e);
            }

            DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
        }
    };//PullCallback end

PullCallback执行,即DefaultMQPushConsumerImpl$1执行把拉取的消息保存到MessageQueue对应的处理队列ProcessQueue,然后由消费客户端进行消费,分并发消费和顺序消费

3.1.并发消费

并发消费入口是在pullcallback内,org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService.submitConsumeRequest(List<MessageExt>, ProcessQueue, MessageQueue, boolean),功能就是把拉取到的每个消息包装为task ConsumeRequest,然后丢入到消费端线程池进行并发消费

@Override
public void submitConsumeRequest(
    final List<MessageExt> msgs,//拉取到的消息集合
    final ProcessQueue processQueue,
    final MessageQueue messageQueue,
    final boolean dispatchToConsume) {
    final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();//代码@1 批量消费的数量,默认1
    if (msgs.size() <= consumeBatchSize) {//消息只拉取到一个
        ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue);
        try {
            this.consumeExecutor.submit(consumeRequest);
        } catch (RejectedExecutionException e) {
            this.submitConsumeRequestLater(consumeRequest);
        }
    } else {//消息默认拉取到32条
        for (int total = 0; total < msgs.size(); ) {//代码@2
            List<MessageExt> msgThis = new ArrayList<MessageExt>(consumeBatchSize);
            for (int i = 0; i < consumeBatchSize; i++, total++) {//代码@3
                if (total < msgs.size()) {
                    msgThis.add(msgs.get(total));
                } else {
                    break;
                }
            }

            ConsumeRequest consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue);//代码@4
            try {
                this.consumeExecutor.submit(consumeRequest);//代码@5 每个consumeRequest丢入到线程池处理,那么就并发消费这拉取到的32个消息了
            } catch (RejectedExecutionException e) {//如果消费端的速度跟不上,导致消费线程池reject,则进行批量消费
                for (; total < msgs.size(); total++) {//代码@6
                    msgThis.add(msgs.get(total));
                }

                this.submitConsumeRequestLater(consumeRequest);//代码@6
            }
        }
    }
}

解释说明:

代码@1:获取消费客户端的默认单次消费消息的个数,默认是1,可以设置DefaultMQPushConsumer.setConsumeMessageBatchMaxSize(int)设置为批量消费。

代码@2:遍历消息的数量

代码@3:按照消费的批次个数设置一个消费线程要消费的消息集合,默认是1个消息

代码@4:待消费的消息集合(默认一条消息)、处理队列、消息队列包装为task对象ConsumeMessageConcurrentlyService.ConsumeRequest

代码@5:把task对象丢入到消费线程池处理,这是多个线程并发执行,因此叫并发消费。消费端的线程池是ConsumeMessageConcurrentlyService.consumeExecutor,默认是20~64个消费线程,如果业务代码消费消息速度慢,可以在消费客户端进行设置较大的消费线程池。

代码@6:消费速度跟不上拉取速度导致消费线程池报reject,则单个消息消费变为批量消费,遇到这样问题,就需要调整消费客户端的消费线程池了,或者查看客户端消费速度慢的原因。因此在客户端消费的业务代码内,不能只是msgs.get(0)处理,要进行遍历处理。

下面看并发消费的具体逻辑org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService.ConsumeRequest.run()

@Override
public void run() {
    if (this.processQueue.isDropped()) {//pq被抛弃,则不执行实际业务逻辑消费,被抛弃的原因比如消费端发生变化,rebalance线程重新负载了
        log.info("the message queue not be able to consume, because it's dropped. group={} {}", ConsumeMessageConcurrentlyService.this.consumerGroup, this.messageQueue);
        return;
    }

    MessageListenerConcurrently listener = ConsumeMessageConcurrentlyService.this.messageListener;//业务消费中的消费监听器 ack机制
    ConsumeConcurrentlyContext context = new ConsumeConcurrentlyContext(messageQueue);
    ConsumeConcurrentlyStatus status = null;
    
    //忽略钩子方法
    
    long beginTimestamp = System.currentTimeMillis();
    boolean hasException = false;
    ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
    try {
        ConsumeMessageConcurrentlyService.this.resetRetryTopic(msgs);//如果msg的topic是%RETRY%,则说明是消费失败的重发消息,更新msg的topic为原始topic
        if (msgs != null && !msgs.isEmpty()) {
            for (MessageExt msg : msgs) {
                MessageAccessor.setConsumeStartTimeStamp(msg, String.valueOf(System.currentTimeMillis()));//更新msg的CONSUME_START_TIME属性为当前时间戳
                /*
                 * 为什么要设置当前时间戳呢?是因为防止消息超过60s还没被消费,在消费客户端启动的时候启动一个计划线程,每15s执行一次ConsumeMessageConcurrentlyService.cleanExpireMsg(),
                 * 功能就是遍历ProcessQueue中保存的消息集合,如果第一条消息的CONSUME_START_TIME距离当前时间戳超过了60s,则从pq上移除,并回发到broker。
                 */
            }
        }
        status = listener.consumeMessage(Collections.unmodifiableList(msgs), context);//业务代码执行消息消费
    } catch (Throwable e) {
        log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}",
            RemotingHelper.exceptionSimpleDesc(e),
            ConsumeMessageConcurrentlyService.this.consumerGroup,
            msgs,
            messageQueue);
        hasException = true;
    }
    
    //忽略不重要代码
    
    if (!processQueue.isDropped()) {//processQueue未抛弃
    	/*
    	 * 处理消费结果
    	 */
        ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this);
    } else {
        log.warn("processQueue is dropped without process consume result. messageQueue={}, msgs={}", messageQueue, msgs);
    }
}

run()方法代码分三步

step1:消息主题是%RETRY%,则恢复消息topic

step2:执行业务代码消费

step3:pq未被废弃,处理消费结果,这个是核心方法,下面看这个方法

public void processConsumeResult(
        final ConsumeConcurrentlyStatus status,
        final ConsumeConcurrentlyContext context,
        final ConsumeRequest consumeRequest
    ) {
        int ackIndex = context.getAckIndex();//默认是Integer.MAX_VALUE

        if (consumeRequest.getMsgs().isEmpty())//待消费的消息是空,则不处理
            return;

        switch (status) {
            case CONSUME_SUCCESS:
                if (ackIndex >= consumeRequest.getMsgs().size()) {
                    ackIndex = consumeRequest.getMsgs().size() - 1;//消费成功,ackIndex赋值为消费的消息条数-1,即通常是消费单个消息,那么就是0
                }
                int ok = ackIndex + 1;//消费单个消息情况是1,批量消费是本次run()执行消费的消息条数
                int failed = consumeRequest.getMsgs().size() - ok;//失败是0
                // 统计成功/失败数量
                this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), ok);
                this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), failed);
                break;
            case RECONSUME_LATER:
                ackIndex = -1;
                this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(),
                    consumeRequest.getMsgs().size());
                break;
            default:
                break;
        }

        switch (this.defaultMQPushConsumer.getMessageModel()) {
            case BROADCASTING:
                for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
                    MessageExt msg = consumeRequest.getMsgs().get(i);
                    log.warn("BROADCASTING, the message consume failed, drop it, {}", msg.toString());
                }
                break;
            case CLUSTERING:
                List<MessageExt> msgBackFailed = new ArrayList<MessageExt>(consumeRequest.getMsgs().size());
                //在批量消费中如果设置了ConsumeConcurrentlyContext.ackIndex,那么就会从失败处开始重复消费,而非从该批量的开始重复消费
                /*
                 * 对於单条消费且消费成功,ackIndex=0,那么i=1开始,则不进入for循环
                 * 对于批量消费且消费成功,ackIndex=消费条数,那么i从消费的消息条数开始,因此也不进入for循环
                 * 因此对于消费成功,无论单条消费or批量消费,都不进入for循环
                 * 
                 * 对於单条消费且消费失败,ackIndex=-1,那么i=0开始,则进入for循环,循环一次
                 * 对于批量消费且消费失败,ackIndex=-1,那么i=0开始,则进入for循环,循环次数为消息的总条数。
                 * 
                 * 问题:那么对于批量消费,比如32条,那么消费到第32条的时候消费失败了,那么这次消费的消息要全部回发到broker,
                 * 	然后消费端又重新消费了前面31条,这样是不好的,可否有从消费失败处回发呢?可以的,在业务代码内设置
                 * 	ConsumeConcurrentlyContext.setAckIndex(int)即可,设置为消费失败的位置,这样
                 * 	消费失败就会从消费失败的消息位置进行回发到broker继而被消费端消费,就避免了批量消费重复消费成功的消息。
                 * 
                 */
                for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {//消费成功不走这里,消费失败走这里
                    MessageExt msg = consumeRequest.getMsgs().get(i);
                    boolean result = this.sendMessageBack(msg, context);//消息回发到broker,主题是%RETRY%+topicName,result为true表示回发成功
                    if (!result) {//回发broker失败
                        msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);//设置msg的重复消费次数+1
                        msgBackFailed.add(msg);//保存该msg到msgBackFailed,供下面重新消费
                    }
                }

                if (!msgBackFailed.isEmpty()) {//重发消息发送到broker失败的情况下
                    consumeRequest.getMsgs().removeAll(msgBackFailed);//把消费失败的消息从consumeRequest移除,这里对应代码@1

                    //消费task重新执行消费失败且回发到broker失败的消息,延时5s执行
                    this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
                }
                break;
            default:
                break;
        }
        
        //把消费成功的消息从ProcessQueue移除,并返回该批消息的最小offset
        long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());//代码@1
        if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {		//如果ProcessQueue失效了(在Reblance线程中pull动作超过120s置为失效),那么就更新consumerqueue对象的offset更新为消费前offset,这样做就是表示了失败从头开始
            this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);//这里就会导致重复消费
        }
    }

代码加的很清晰了,而且对于批量消费失败消费从消费失败位置如何进行回发也说明了。removceMessage方法返回pq.msgTreeMap上保存的消息最小的offset,然后在updateOffset操作内把offset保存到消费客户端RemoteBrokerOffsetStore.offsetTable,消费客户端记录的消费offset在线程Thread [MQClientFactoryScheduledThread]每5s执行MQClientInstance.persistAllConsumerOffset()内保存到broker,发送命令是UPDATE_CONSUMER_OFFSET。

processConsumeResult方法的核心功能是for循环和代码@1处removeMessage,下面看代码@1

public long removeMessage(final List<MessageExt> msgs) {
        long result = -1;
        final long now = System.currentTimeMillis();
        try {
            this.lockTreeMap.writeLock().lockInterruptibly();//加写锁,因为要对红黑树进行写操作
            this.lastConsumeTimestamp = now;
            try {
                if (!msgTreeMap.isEmpty()) {//msgTreeMap是每次pull到消息后保存的本次pull到的消息(默认一次拉取32条消息)
                    result = this.queueOffsetMax + 1;//代码@2	this.queueOffsetMax保存的是拉取到的32条消息中offset最大的,在ProcessQueue.putMessage(List<MessageExt>)设置,该方法是拉取到消息后在PullCallback内调用
                    int removedCnt = 0;
                    for (MessageExt msg : msgs) {//遍历本次消费的消息集合,通常是一个消息,因为默认一次消费消费一个消息
                        MessageExt prev = msgTreeMap.remove(msg.getQueueOffset());//从红黑树移除被消费的消息
                        if (prev != null) {//说明被消费的消息在pq内
                            removedCnt--;//计数器
                            msgSize.addAndGet(0 - msg.getBody().length);//pq.msgSize减去本次消费的消息size
                        }
                    }
                    msgCount.addAndGet(removedCnt);//pq.msgCount消息数量减去被消费的消息数量

                    if (!msgTreeMap.isEmpty()) {//pq上还有消息,说明拉取到的32条消息还没被消费完,则返回拉取到的消息集合第一个消息offset,即最小offset最。这也说明了为什么用红黑树保存拉取到的消息了,按照消息的offset排序,这次消费一条消息,返回最小的offset,这样避免了消息丢失(offset大的先被执行消费完毕)
                        result = msgTreeMap.firstKey();//代码@1	返回pq上第一个消息的offset,即最小offset
                    }
                }
            } finally {
                this.lockTreeMap.writeLock().unlock();//finally 释放锁
            }
        } catch (Throwable t) {
            log.error("removeMessage exception", t);
        }

        return result;
    }

该方法有些难理解,重点是代码@1、代码@2处,对于每次消费成功,从pq移除该消息,如果pq还有消息(多个消费线程消费同一个ProcessQueue.msgTreeMap集合上保存的消息),那么返回最小的offset,如果pq上没有待消费的消息了,则返回ProcessQueue.queueOffsetMax(该属性保存的是一次拉取到的消息中的max offset),这样既避免了消息遗漏的情况,最终又保存到了消费最大offset的情况。因此完美解决了 对于并发消费,消费msg1,msg2,msg3,它们的offset依次是增加的,在消费成功后,msg3先被消费完,继而保存offset的时候还是保存的msg1的offset,而非msg3.offset,这样避免了消费时候消息遗问题,但是会导致有重复消费的可能,当然rmq并不保证重复消费,由业务保证。

FIXME:唯一我不明白的是代码@2处result = this.queueOffsetMax + 1;,为什么要+1呢?我认为是result = this.queueOffsetMax就行了,应该是我理解的这个哪里有问题?后续明白了更新。解决:该offset不是具体的在consumequeue上的物理偏移量,而是表示在consumequeue上是第几条消息,因此需要+1,从下条消息开始进行消息拉取。

至此并发消费写完了,但是并没有写上消费失败的时候,消息回发到broker的情况,后续在延时消息写。

3.2.顺序消息消费

rocketmq的顺序消息并不是严格的顺序,只是分区顺序,把一个生产者产生的消息按照消息产生顺序存放到同一个mq上,那么这样就涉及到发送的时候对待存放的消息队列的选择了,因此需要实现MessageQueueSelector来选择要发送的消息队列,其他发送同普通消息发送。顺序消息的定义参考https://help.aliyun.com/document_detail/49319.html?spm=a2c4g.11186623.6.553.4ff06b450u63ex

 

顺序消费的启动和并发消费的启动基本相同,在前面的图已经画出来了,顺序消费ConsumeMessageOrderlyService,task是ConsumeMessageOrderlyService.ConsumeRequest,顺序消费主要是要对消费的mq进行加锁,重新负载后还要对mq解锁。

在PullMessageService服务线程拉取到消息后,执行PullCallback.onSuccess()时同并发消费一样把拉取到的消息保存到ProcessQueue.msgTreeMap,而后在org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService.submitConsumeRequest(List<MessageExt>, ProcessQueue, MessageQueue, boolean)内把processQueue, messageQueue包装创建为task ConsumeMessageOrderlyService.ConsumeRequest丢入到顺序消费线程池(min20 max64)处理,顺序消费一个messagequeue只会在一个work线程上执行,因此一个消费客户端对于顺序消费是串行执行,不存在并发。

接着看ConsumeMessageOrderlyService.ConsumeRequest的执行逻辑run()方法

//org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService.ConsumeRequest.run()
public void run() {
    if (this.processQueue.isDropped()) {
        log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
        return;
    }

    final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);//从缓存获取一个对象,在该对象上同步
    synchronized (objLock) {//加锁同步
    	/*
    	 * this.processQueue.isLocked()被加锁了且锁时间未失效!this.processQueue.isLockExpired(),pq加锁并设置时间戳是在负载服务线程内设置的,还有是在计划任务ConsumeMessageOrderlyService.lockMQPeriodically()设置
    	 * 广播消费模式进入执行,or 集群消费模式且pq在有效加锁时间内进入
    	 */
        if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
            || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {
            final long beginTime = System.currentTimeMillis();
            for (boolean continueConsume = true; continueConsume; ) {
                if (this.processQueue.isDropped()) {//pq失效,则退出for循环,不做消费
                    log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
                    break;
                }

                //忽略不重要代码
                
                final int consumeBatchSize =
                    ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();//默认1,可以设置一次消费的消息数量

								//从pq.msgTreeMap上移除offset最小的consumeBatchSize条消息返回(默认返回一个消息),同时把这些消息保存到pq.consumingMsgOrderlyTreeMap这个红黑树上
                List<MessageExt> msgs = this.processQueue.takeMessags(consumeBatchSize);//顺序消费
                if (!msgs.isEmpty()) {
                    final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue);

                    ConsumeOrderlyStatus status = null;

                    //忽略不重要代码
                    
                    long beginTimestamp = System.currentTimeMillis();
                    ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
                    boolean hasException = false;
                    try {
                        this.processQueue.getLockConsume().lock();//pq的重入锁加锁,保证只有一个线程可以消费该pq上的该消息
                        if (this.processQueue.isDropped()) {
                            log.warn("consumeMessage, the message queue not be able to consume, because it's dropped. {}",
                                this.messageQueue);
                            break;
                        }

                        //业务代码执行消费消息
                        status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context);
                    } catch (Throwable e) {
                        log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}",
                            RemotingHelper.exceptionSimpleDesc(e),
                            ConsumeMessageOrderlyService.this.consumerGroup,
                            msgs,
                            messageQueue);
                        hasException = true;
                    } finally {
                        this.processQueue.getLockConsume().unlock();//pq的重入锁解锁
                    }

                    //忽略不重要代码

                    //更新offset成功则继续从pq拉取消息消费(继续执行for循环),这个一个顺序消费线程就消费完了pull到的所有消息
                    continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);
                } else {//pq无待消费消息,task退出执行
                    continueConsume = false;
                }
            }
        } else {
            if (this.processQueue.isDropped()) {
                log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
                return;
            }
          //pq未被加锁or锁时间失效,稍后再重新消费
            ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100);
        }
    }
}

在去除了一些钩子方法和统计后,方法很简明了,分为三步
step1:从pq上获取待消费的消息,默认是一条,可以设置多条。

step2:业务代码消费,消费结果是ConsumeOrderlyStatus.SUCCESS、ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT两种,前者成功,后者消费失败。

step3:processConsumeResult处理消费结果,处理成功接着继续从pq拉取消息进行消费。下面看该方法

/*
 * 传入参数msgs是被消费的消息,status是消费结果,context是MessageListenerOrderly.consumeMessage(List<MessageExt>, ConsumeOrderlyContext)中的第二个参数,业务代码实现该方法,consumeRequest是ConsumeMessageOrderlyService.ConsumeRequest
 * 功能:处理消费结果,消费成功更新消费客户端本地的offset,消费失败,则把消息重新放到pq.msgTreeMap上,然后阻塞在该消息,接着继续消费该消息。
 */
public boolean processConsumeResult(
    final List<MessageExt> msgs,
    final ConsumeOrderlyStatus status,
    final ConsumeOrderlyContext context,
    final ConsumeRequest consumeRequest
) {
    boolean continueConsume = true;
    long commitOffset = -1L;
    if (context.isAutoCommit()) {//默认true,用于非事务消息,默认执行这里
        switch (status) {
            case COMMIT:
            case ROLLBACK:
                log.warn("the message queue consume result is illegal, we think you want to ack these message {}",
                    consumeRequest.getMessageQueue());
            case SUCCESS:
                commitOffset = consumeRequest.getProcessQueue().commit();//获取本次消费的消息中的消息最大offset
                this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
                break;
            case SUSPEND_CURRENT_QUEUE_A_MOMENT:
                this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
                /*
						     * checkReconsumeTimes返回true,则说明消费失败次数未达到最大  or 达到最大消费失败次数但是回发broker失败。
						     * 	返回false说明消费失败达到最大次数且回发该消息到broker成功
						     */
                if (checkReconsumeTimes(msgs)) {//检测重复消费次数,返回true,则说明消费失败次数未达到最大  or 达到最大消费失败次数但是回发broker失败
                    consumeRequest.getProcessQueue().makeMessageToCosumeAgain(msgs);//代码@1 把消息重新放到ProcessQueue.msgTreeMap,这样task再次执行还是获取到当前消费失败的消息,继而就是阻塞了,因此需要设置最大消费失败次数,不然消费失败一直阻塞在该消息上了。
                    this.submitConsumeRequestLater(
                        consumeRequest.getProcessQueue(),
                        consumeRequest.getMessageQueue(),
                        context.getSuspendCurrentQueueTimeMillis());//延时1s继续执行task消费任务
                    continueConsume = false;
                } else {//消费失败次数达到了最大且回发broker成功执行这里,即暂时跳过该消费失败的消息消费后续消息,因此返回offset
                    commitOffset = consumeRequest.getProcessQueue().commit();
                }
                break;
            default:
                break;
        }
    } else {//业务代码内设置了手动提交,用于事务消息
        //省略事务消息处理
    }

    if (commitOffset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
        this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), commitOffset, false);//注意顺序消费updateOffset操作是false,那么就有可能消费达到最大失败次数回发到broker后,又被重新拉取到而消费,那么为false的情况,就会把消费端本地保存的offset更新为旧的offset,导致重复消费。因此顺序消费,消费失败达到最大失败次数情况下,直接返回消费成功,记录db,不回发broker。
    }

    return continueConsume;
}

顺序消费,消费失败的时候是被阻塞的,消费失败后,然后当前在运行的task就退出,消息又重新被保存到pq,新创建ConsumeRequest提交到线程池,默认延时1s后再次消费,这个延时时间可以业务代码内调整。

总结:顺序消费在业务代码要设置最大失败消费次数,达到这个次数,把消息保存到db,而后要返回消费成功,这样避免了消息回发到broker到死信队列,这样做比较方便。

以上是一个MessageQueue的消费情况,那么一个消费客户端对应消费多个mq呢?

解答:PullMessageService拉取消息是按照PullRequest来拉取的,一个PullRequest表示一个消息队列mq,那么在一个消费端被分配了多个mq的时候,每个mq拉取到的消息都会丢入到线程池处理(无论并发消费or顺序消费都默认是20~64个线程),并发消费是多个消费线程一起执行,这个容易理解,但是顺序消费必须要串行执行,是如何做的呢?答案就在上面分析的org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService.ConsumeRequest.run()代码的for循环上,顺序消费和并发消费不同的是并发消费的task是ConsumeMessageConcurrentlyService.ConsumeRequest,它包装了待消费的消息,因此可以在线程池中并发执行。但是ConsumeMessageOrderlyService.ConsumeRequest是不包含待消费的消息,而是在运行过程中从processqueue上拉取消息然后进行消费,消费完毕后,接着再进行拉取消息,因此虽然顺序消费的线程池的work线程是多个,但是实际上一个mq的消费只会同时只有线程池中的一个work线程执行,因此做到了顺序消费是串行的。

至此顺序消费写完。

 

3.3.延时消费

RocketMQ 支持定时消息,但是不支持任意时间精度,仅支持特定的 level,例如定时 5s, 10s, 1m 等。其中,level=0 级表示不延时,level=1 表示 1 级延时,level=2 表示 2 级延时,以此类推。
如何配置:在broker的属性配置文件中加入以下行:
默认是messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
描述了各级别与延时时间的对应映射关系。
这个配置项配置了从1级开始各级延时的时间,如1表示延时1s,2表示延时5s,14表示延时10m,可以修改这个指定级别的延时时间;?
时间单位支持:s、m、h、d,分别表示秒、分、时、天;?
默认值就是上面声明的,可手工调整

在rmq中每个延时级别对应一个mq,默认是18个延时级别,则是18个mq,主题是SCHEDULE_TOPIC_XXXX

先说下延时消息的发送,有producer发送延时消息、并发消费失败回发消息到broker,顺序消费失败次数超过最大回发broker,这些情况都会保存到延时主题上。

并发消费失败回发、顺序消费失败回发、延时消息发送broker端处理异同如图

并发消费是消费失败就回发到broker,顺序消费是消费次数达到了最大失败次数才回发到broker,两者发送命令不同,在broker端SendMessageProcessor处理器的方法不同,但是相同的是消费重试消息都会被保存到SCHEDULE_TOPIC_XXXX主题对应的延时mq内。producer发送延时消息和顺序消费重发级别相同,不同的是不需要延时消息发送的是原topic,而顺序消费重试回发发送的是%RETRY%consumegroup。 最终不论是延时消息or retry消息,都是被保存到SCHEDULE_TOPIC_XXXX上,队列就是各自的延时级别,因为消费端不订阅SCHEDULE_TOPIC_XXXX,因此自然延时消息就无法被消费了。

那么延时消息是如何被消费的?肯定需要把SCHEDULE_TOPIC_XXXX的消息改为原topic才可以消费,在哪里进行的呢?

在broker启动的时候org.apache.rocketmq.store.DefaultMessageStore.start()内执行org.apache.rocketmq.store.schedule.ScheduleMessageService.start(),该方法内启动一个timer线程Thread[ScheduleMessageTimerThread],该线程对每个队列任务DeliverDelayedMessageTimerTask执行org.apache.rocketmq.store.schedule.ScheduleMessageService.DeliverDelayedMessageTimerTask.run(),该run()方法每执行一次后又会重新创建DeliverDelayedMessageTimerTask再执行执行,我们就把它当作一个计划任务即可,逻辑在org.apache.rocketmq.store.schedule.ScheduleMessageService.DeliverDelayedMessageTimerTask.executeOnTimeup()方法内

//org.apache.rocketmq.store.schedule.ScheduleMessageService.DeliverDelayedMessageTimerTask.executeOnTimeup()
public void executeOnTimeup() {
    ConsumeQueue cq =
        ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(SCHEDULE_TOPIC,
            delayLevel2QueueId(delayLevel));//根据延时topic SCHEDULE_TOPIC_XXXX和延时队列获取consumequeue

    long failScheduleOffset = offset;//当前延时级别对应的mq的offset,该offset并不是在consumequeue上的物理位置,而是第几条消息的意思

    if (cq != null) {
        SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);//返回从该offset所归属的MappedFile对象上从offset开始到consumequeeu的写位置之间的缓冲区
        if (bufferCQ != null) {
            try {
                long nextOffset = offset;
                int i = 0;
                ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();//忽略
                /*
                 * 从consumequeue的延时队列读取一条消息,如果到了要发起的时间,则把消息还原topic,并写入到commitlog,
                 *	然后reputmessageservice线程会转储到consumequeue中,这样消费端就可以消费了。
                 * 	这样for循环下就可以把发起时间到了的消息都发起保存到commitlog供消费了。
                 */
                for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
                    long offsetPy = bufferCQ.getByteBuffer().getLong();//commitlog offset 8
                    int sizePy = bufferCQ.getByteBuffer().getInt();//msg size 4
                    long tagsCode = bufferCQ.getByteBuffer().getLong();//时间戳 8,对于延时消息consumequeue存放的不是taghash而是具体发起时间

                    //忽略扩展的cq
                    
                    long now = System.currentTimeMillis();
                    long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);//tagsCode>now+延时级别对应的延时时间,说明到了发起时间,则返回now值,否则返回tagsCode值

                    nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);

                    long countdown = deliverTimestamp - now;

                    if (countdown <= 0) {//到时间了需要发起的消息
                        MessageExt msgExt =
                            ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(
                                offsetPy, sizePy);//根据消息offset和size从commitlog查询到消息返回,该消息的主题是SCHEDULE_TOPIC_XXXX

                        if (msgExt != null) {
                            try {
                                MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);//返回新的消息msgInner,还原了原topic queueid,清除了消息的属性PROPERTY_DELAY_TIME_LEVEL,这样就是个普通消息了,不再具有延时
                                PutMessageResult putMessageResult =
                                    ScheduleMessageService.this.defaultMessageStore
                                        .putMessage(msgInner);//把原消息追加到commitlog中,即该消息的topic是待消费的topic

                                if (putMessageResult != null
                                    && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
                                    continue;//原消息追加到commitlog成功,接着进行for循环
                                } else {
                                    // XXX: warn and notify me
                                    log.error(
                                        "ScheduleMessageService, a message time up, but reput it failed, topic: {} msgId {}",
                                        msgExt.getTopic(), msgExt.getMsgId());
                                    ScheduleMessageService.this.timer.schedule(
                                        new DeliverDelayedMessageTimerTask(this.delayLevel,
                                            nextOffset), DELAY_FOR_A_PERIOD);
                                    ScheduleMessageService.this.updateOffset(this.delayLevel,
                                        nextOffset);
                                    return;
                                }
                            } catch (Exception e) {
                                log.error(
                                    "ScheduleMessageService, messageTimeup execute error, drop it. msgExt="
                                        + msgExt + ", nextOffset=" + nextOffset + ",offsetPy="
                                        + offsetPy + ",sizePy=" + sizePy, e);
                            }
                        }
                    } else {//消息未到发起时间,重新执行task
                        ScheduleMessageService.this.timer.schedule(
                            new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset),
                            countdown);
                        ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);//更新该延时队列的消费offset到ScheduleMessageService.offsetTable
                        return;
                    }
                } // end of for

               
                nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
                //task 100ms后执行,这里task是新的offset
                ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
                    this.delayLevel, nextOffset), DELAY_FOR_A_WHILE);
                //遍历完当前延时队列发起的消息,更新offset位置到ScheduleMessageService.offsetTable
                ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
                return;
            } finally {

                bufferCQ.release();//是否缓冲区
            }
        } // end of if (bufferCQ != null)
        else {//不存在consumequeueu

            long cqMinOffset = cq.getMinOffsetInQueue();
            if (offset < cqMinOffset) {
                failScheduleOffset = cqMinOffset;
                log.error("schedule CQ offset invalid. offset=" + offset + ", cqMinOffset="
                    + cqMinOffset + ", queueId=" + cq.getQueueId());
            }
        }
    } // end of if (cq != null)
	//cq==null,即根据offset未找到cq,延时100ms重新执行task
    ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel,
        failScheduleOffset), DELAY_FOR_A_WHILE);
}

代码具体含义看注释,该方法的功能就是根据延时队列的offset找到延时队列,读取消息commitlog offset ,size,然后到commitlog读取到具体的SCHEDULE_TOPIC_XXXX消息,然后把消息还原为原topic并追加到commitlog,这样客户端就可以消费消息了。

延时消息的几种发送情况说明白了,那么对于消费而言跟普通消息消费是完全相同的,也可以看出顺序/并发消费失败超过最大次数回发broker会被保存到死信队列,死信队列默认还不能度(可以使用mqadmin命令修改为可读),因此业务上对于顺序/并发消费在失败超过最大次数了要保存到db,返回消费成功,避免发送到死信情况。

至此延时消息说完。

思考:为什么rmq中有许多计划任务是使用的Timer而非ScheduledThreadPoolExecutor这个计划线程池呢?timer内部只是包含一个线程,可以使用ScheduledThreadPoolExecutor的时候也只是一个线程,这个为什么不使用ScheduledThreadPoolExecutor呢?Timer已经不建议使用了,这个暂时不清楚?

 

3.4.事务消息

比如一个下订单扣库存的动作,这两个服务分别操作订单库和库库,属于分布式事务范畴了,如果mq不支持事务,那么可能做法是:

//step1:开启本地事务

//step2:订单库新增一条记录

//step3:向mq发送订单消息,用于扣库存

//step4:提交事务/回滚事务

该方案在正常情况下没有问题,但是一些异常情况下就有了问题:

1.如果step3执行后,在step4执行前jvm进程or服务器宕机,事务没有成功提交,订单库没变化和但是库存库减少,导致两个库数据不一致

2.由于消息是在事务提交之前提交,发送的消息内容是订单实体的内容,会造成在消费端进行消费时如果需要去验证订单是否存在时可能出现订单不存在,该问题也会存在,因为消费端速度很快的话。

对于生成订单(DB操作)和发送消息是一个事务内的动作,因此要保证要么全部成功,要么回滚,因此可以采用rocketmq的事务消息来解决。

 

rocketmq事务消息解决分布式事务,实现最终数据一致性,思想就是xa协议2pc,整体交互流程如下图所示(图片来源网上,该图很清晰明了,如果前面的消息发送和消费看懂了,事务消息也很容易明白)

所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败。来源网上,觉得说的好,帖出来了。

事务消息是使用TransactionMQProducer进行发送的,和普通消息的发送者producer不同的是需要业务开发自定义线程池和org.apache.rocketmq.client.producer.TransactionListener的实现

下面开始看代码(事务消息发送客户端参考rocketmq的example下的代码)

先贴图,先看事务producer的启动

和普通消息producer启动基本相同,只是增加了事务监听器和事务检查线程池,分别用于执行事务、检查事务和接收broker发来的事务回查请求。

接着看事务消息的发送处理流程图

 

该图把一些处理细节给标注了,可以跟前面的producer发送泳道图比较,看看和事务消息producer有什么区别。

该过程中的核心点是EndTransactionProcessor.processRequest(ChannelHandlerContext, RemotingCommand),下面看该方法代码

//org.apache.rocketmq.broker.processor.EndTransactionProcessor.processRequest(ChannelHandlerContext, RemotingCommand)
public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws
    RemotingCommandException {
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    /*
     *	 该requestHeader包含了
     *	producerGroup==producerGroupname,
     *	tranStateTableOffset==prepare消息在consumequeue的位置(表示该消息在cq上是第几条消息), (用于把原始的事务消息保存到consumequeue上,即保存在prepare消息在consumequeue的位置)
     *	commitLogOffset==prepare消息在commitlog的绝对位置,(用于查找在commitlog上的commitLogOffset位置的prepare消息,把prepare消息转换为原始消息,继而最后保存到commitlog上)
     *	commitOrRollback==事务消息类型TRANSACTION_COMMIT_TYPE/TRANSACTION_ROLLBACK_TYPE/TRANSACTION_NOT_TYPE,
     *	transactionId==消息的 UNIQ_KEY
     *	msgId==消息的UNIQ_KEY
     *	fromTransactionCheck是否是broker回查事务,true是,false否
     */
    final EndTransactionRequestHeader requestHeader =
        (EndTransactionRequestHeader)request.decodeCommandCustomHeader(EndTransactionRequestHeader.class);
    LOGGER.info("Transaction request:{}", requestHeader);
    if (BrokerRole.SLAVE == brokerController.getMessageStoreConfig().getBrokerRole()) {//不允许salve broker处理事务消息
        response.setCode(ResponseCode.SLAVE_NOT_AVAILABLE);
        LOGGER.warn("Message store is slave mode, so end transaction is forbidden. ");
        return response;
    }

    if (requestHeader.getFromTransactionCheck()) {//表示是否是回查检查消息。用于broker发producer消息回查事务,producer结束事务发送到broker的时候,该值为true。对于producer发送prepare消息后执行完本地事务,发送commit/rollback消息到broker的时候,该值为false。
        //回查事务和非回查,执行功能是一样的
    	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: {//对应事务状态的UNKNOW,不处理
                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: {//事务commit消息,处理
                break;
            }

            case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: {//事务rollback消息,处理
                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()) {
    	//事务commit消息,则直接将原先发的prepare从commitlog文件读出来消息转换为原消息,并写入commitlog,消息的topic是原topic,即被消费者订阅可以消费到
        result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);//根据EndTransactionRequestHeader.commitLogOffset这个commitlog物理偏移量从commitlog中查找到prepare消息
        if (result.getResponseCode() == ResponseCode.SUCCESS) {//从commitlog中查找到了prepare消息
            RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);//检测prepare消息和收到的EndTransactionRequestHeader.commitlogOffset等信息是否匹配
            if (res.getCode() == ResponseCode.SUCCESS) {//检测通过
                MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());//还原prepare消息的topic queueid等信息为原始消息
                msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback()));//sysflag更新为TRANSACTION_COMMIT_TYPE
                msgInner.setQueueOffset(requestHeader.getTranStateTableOffset());//设置原始消息在consumequeue的offset,即保存到prepare消息在consumequeue上的位置。
                msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());//prepare消息在commitlog的绝对(物理)位置,即commitlog格式中的PTO
                msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());//原始消息的存储时间戳为prepare消息存储时间戳
                RemotingCommand sendResult = sendFinalMessage(msgInner);//把原始消息写入到commitlog
                if (sendResult.getCode() == ResponseCode.SUCCESS) {//原始消息写入commitlog成功,从commitlog删除prepare消息
                    this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());//所谓删除prepare消息就是把该消息写入到commitlog,topic是op half topic,这样broker回查的时候判断OP HALF有了该消息,就不再进行回查
                }
                return sendResult;
            }
            return res;
        }
    } else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
        //如果是Rollback,则直接将消息转换为原消息,并写入到Op Topic里
    	result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);//根据EndTransactionRequestHeader.commitLogOffset这个commitlog物理偏移量从commitlog中查找到prepare消息
        if (result.getResponseCode() == ResponseCode.SUCCESS) {
            RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);//检测prepare消息和收到的EndTransactionRequestHeader.commitlogOffset等信息是否匹配
            if (res.getCode() == ResponseCode.SUCCESS) {
                this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());//所谓删除prepare消息就是把该消息写入到commitlog,topic是op half topic
            }
            return res;
        }
    }
    response.setCode(result.getResponseCode());
    response.setRemark(result.getResponseRemark());
    return response;//设置返回结果,实际producer是oneway发送方式,不返回producer
}

代码加了注释,比较容易懂,大体流程就是:

commit消息=>从commitlog读取出prepare消息=>检查prepare消息=>转换为真正待消费消息=>追加到commitlog文件=>删除prepare消息=>ReputMessageService把待消费消息转储到consumequeue=>客户端消费事务消息。

rollback消息=>从commitlog读取出prepare消息=>检查prepare消息=>删除prepare消息。

该方法的核心就是根据EndTransactionRequestHeader上送的commitlogPhysOffset找到prepare消息,然后还原消息保存到commitlog内,也很容易理解。那么commitlogPhysOffset如来的,还得根据代码自己找,下面我总结了下EndTransactionRequestHeader的属性,如果找不清楚来源的,可以参考下,

public class EndTransactionRequestHeader implements CommandCustomHeader {
    @CFNotNull
    private String producerGroup;//发送broker前赋值	producerGroupname
    @CFNotNull
    private Long tranStateTableOffset;//发送broker前赋值	prepare消息在consumequeue的位置(表示该消息在cq上是第几条消息)
    @CFNotNull
    private Long commitLogOffset;//发送broker前赋值  prepare消息在commitlog的绝对位置
    @CFNotNull
    private Integer commitOrRollback; // TRANSACTION_COMMIT_TYPE	发送broker前赋值 为对应的消息类型commit/rollback/unknow
    // TRANSACTION_ROLLBACK_TYPE
    // TRANSACTION_NOT_TYPE

    @CFNullable
    private Boolean fromTransactionCheck = false;

    @CFNotNull
    private String msgId;//发送broker前赋值	消息属性的UNIQ_KEY

    private String transactionId;//发送broker前赋值 事务id,通常是消息属性的UNIQ_KEY
}

 

那么问题,如果执行完本地事务后,发送commit消息时候,producer jvm宕机了,那么消息没有发出去,客户端无法消费到,无法扣除库存,导致数据不一致,这应该怎么解决?rmq提供了事务回查功能。

在broker启动的时候启动服务线程Thread [TransactionalMessageCheckService],执行TransactionalMessageCheckService.run(),broker每60s回查producer事务状态,执行堆栈如下图

核心在check方法内,看下面代码和注释

/*
     * 	传入参数:transactionTimeout==60s 事务回查超时时间, transactionCheckMax==15,最大回查次数,listener是DefaultTransactionalMessageCheckListener
     * 	功能:读取当前half的half queueoffset,然后从op half拉取32条消息保存到removeMap,如果half queueoffset处的消息在removeMap中,
     * 		则说明该prepare消息被处理过了,然后读取下一条prepare消息,如果prepare不在removeMap中,说明是需要回查的,此时broker作为client端,向服务端producer发送回查命令,
     * 		最后由producer返回回查结果更新原prepare消息。
     */
    @Override
    public void check(long transactionTimeout, int transactionCheckMax,
        AbstractTransactionalMessageCheckListener listener) {
        try {
            String topic = MixAll.RMQ_SYS_TRANS_HALF_TOPIC;//RMQ_SYS_TRANS_HALF_TOPIC
            Set<MessageQueue> msgQueues = transactionalMessageBridge.fetchMessageQueues(topic);//返回的是half topic的消息队列,只有一个队列
            if (msgQueues == null || msgQueues.size() == 0) {//说明broker还没有接收过prepare消息,自然half topic是null
                log.warn("The queue of topic is empty :" + topic);
                return;
            }
            log.info("Check topic={}, queues={}", topic, msgQueues);
            for (MessageQueue messageQueue : msgQueues) {//遍历half topic下的消息队列,实际只有一个消息队列
                long startTime = System.currentTimeMillis();
                MessageQueue opQueue = getOpQueue(messageQueue);//获取op half topic的消息队列(只有一个队列),OP就是英文operator缩写
                long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue);//获取prepare消息的当前消费queueoffset
                long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue);//获取op half消息的当前消费queueoffset
                log.info("Before check, the queue={} msgOffset={} opOffset={}", messageQueue, halfOffset, opOffset);
                if (halfOffset < 0 || opOffset < 0) {
                    log.error("MessageQueue: {} illegal offset read: {}, op offset: {},skip this queue", messageQueue,
                        halfOffset, opOffset);
                    continue;
                }

                List<Long> doneOpOffset = new ArrayList<>();
                HashMap<Long, Long> removeMap = new HashMap<>();
                //fillOpRemoveMap方法返回的removeMap集合包含的是已经被commit/rollback的prepare消息的queueoffset集合
                PullResult pullResult = fillOpRemoveMap(removeMap, opQueue, opOffset, halfOffset, doneOpOffset);//核心方法
                if (null == pullResult) {
                    log.error("The queue={} check msgOffset={} with opOffset={} failed, pullResult is null",
                        messageQueue, halfOffset, opOffset);
                    continue;
                }
                // single thread
                int getMessageNullCount = 1;//获取空消息的次数
                long newOffset = halfOffset;//当前处理RMQ_SYS_TRANS_HALF_TOPIC#queueId的最新进度。
                long i = halfOffset;//当前处理RMQ_SYS_TRANS_HALF_TOPIC消息的队列偏移量
                while (true) {//遍历,看看queueoffset=i处的prepare消息是否在removeMap集合内,如果在,说明该prepare消息被commit/rollback处理过了,如果不在,则说明该prepare消息未被处理过,需要进行回查
                    if (System.currentTimeMillis() - startTime > MAX_PROCESS_TIME_LIMIT) {//这是RocketMQ处理任务的一个通用处理逻辑,就是一个任务处理,可以限制每次最多处理的时间,RocketMQ为待检测主题RMQ_SYS_TRANS_HALF_TOPIC的每个队列,做事务状态回查,一次最多不超过60S,目前该值不可配置
                        log.info("Queue={} process time reach max={}", messageQueue, MAX_PROCESS_TIME_LIMIT);
                        break;
                    }
                    if (removeMap.containsKey(i)) {//说明i位置处的这个prepare消息已经被commit/rollback处理过了,因此i+1,接着执行下一次while
                        log.info("Half offset {} has been committed/rolled back", i);
                        removeMap.remove(i);
                    } else {
                    	//说明i位置处的这个prepare消息还未被commit/rollback处理过,需要进行回查
                        GetResult getResult = getHalfMsg(messageQueue, i);//从queueoffset位置获取commitlog上的prepare消息,这里的参数i表示queueoffset
                        MessageExt msgExt = getResult.getMsg();//获取queueoffset=i处的prepare消息
                        if (msgExt == null) {//prepare消息不存在
                        	/*
                        	 * 	如果消息为空,则根据允许重复次数进行操作,默认重试一次,目前不可配置。其具体实现为:
                        	 * 1、如果超过重试次数,直接跳出,结束该消息队列的事务状态回查。
                        	 * 2、如果是由于没有新的消息而返回为空(拉取状态为:PullStatus.NO_NEW_MSG),则结束该消息队列的事务状态回查。
                        	 * 3、其他原因,则将偏移量i设置为: getResult.getPullResult().getNextBeginOffset(),重新拉取。
                           	 */
                            if (getMessageNullCount++ > MAX_RETRY_COUNT_WHEN_HALF_NULL) {//空消息次数+1
                                break;
                            }
                            if (getResult.getPullResult().getPullStatus() == PullStatus.NO_NEW_MSG) {//prepare消息不存在,则退出
                                log.info("No new msg, the miss offset={} in={}, continue check={}, pull result={}", i,
                                    messageQueue, getMessageNullCount, getResult.getPullResult());
                                break;
                            } else {//继续从commitlog读取下一个prepare消息
                                log.info("Illegal offset, the miss offset={} in={}, continue check={}, pull result={}",
                                    i, messageQueue, getMessageNullCount, getResult.getPullResult());
                                i = getResult.getPullResult().getNextBeginOffset();
                                newOffset = i;
                                continue;
                            }
                        }

                        /*
                         * needDiscard,prepare消息已经被回查达到15次,则不再回查该prepare消息
                         * needSkip prepare消息存储时间距离现在超过了72h,则不再回查该prepare消息
                         * 	判断该消息是否需要discard(吞没,丢弃,不处理)、或skip(跳过),其依据如下
                         * 	1、needDiscard 依据:如果该消息回查的次数超过允许的最大回查次数,则该消息将被丢弃,即事务消息提交失败,不能被消费者消费,其做法,主要是每回查一次,在消息属性TRANSACTION_CHECK_TIMES中增1,默认最大回查次数为15次。
     					 *	2、needSkip依据:如果事务消息超过文件的过期时间,默认72小时(具体请查看RocketMQ过期文件相关内容),则跳过该消息。
                         */
                        if (needDiscard(msgExt, transactionCheckMax) || needSkip(msgExt)) {//不再回查满足该条件的prepare消息
                            listener.resolveDiscardMsg(msgExt);//打印error日志
                            newOffset = i + 1;
                            i++;
                            continue;//遍历下一个prepare消息
                        }
                        if (msgExt.getStoreTimestamp() >= startTime) {//prepare消息存储时间戳>=broker本次回查开始时间戳,结束回查。说明该prepare消息刚被刷新到commitlog,等待下次再回查该消息
                            log.info("Fresh stored. the miss offset={}, check it later, store={}", i,
                                new Date(msgExt.getStoreTimestamp()));
                            break;
                        }

                        /*
                         * 	处理事务超时相关概念,先解释几个局部变量:
                         * valueOfCurrentMinusBorn :该消息已生成的时间,等于系统当前时间减去消息生成的时间戳。
                         * checkImmunityTime :立即检测事务消息的时间,其设计的意义是,应用程序在发送事务消息后,事务不会马上提交,该时间就是假设事务消息发送成功后,应用程序事务提交的时间,在这段时间内,RocketMQ任务事务未提交,故不应该在这个时间段向应用程序发送回查请求。
                         * transactionTimeout:事务消息的超时时间,这个时间是从OP拉取的消息的最后一条消息的存储时间与check方法开始的时间,如果时间差超过了transactionTimeout,就算时间小于checkImmunityTime时间,也发送事务回查指令。
                         */
                        long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp();//当前时间戳与prepare消息发送时间戳差。bornTimestamp是producer产生的
                        long checkImmunityTime = transactionTimeout;
                        String checkImmunityTimeStr = msgExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS);//源码内没有地方给消息属性设置PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS
                        if (null != checkImmunityTimeStr) {//false
                            checkImmunityTime = getImmunityTime(checkImmunityTimeStr, transactionTimeout);
                            if (valueOfCurrentMinusBorn < checkImmunityTime) {
                                if (checkPrepareQueueOffset(removeMap, doneOpOffset, msgExt)) {
                                    newOffset = i + 1;
                                    i++;
                                    continue;
                                }
                            }
                        } else {//程序走该分支	如果当前时间还未过(应用程序事务结束时间),则跳出本次回查处理的,等下一次再试
                            if ((0 <= valueOfCurrentMinusBorn) && (valueOfCurrentMinusBorn < checkImmunityTime)) {//消息存储时间戳在距离当前时间是60s内,则不回查
                                log.info("New arrived, the miss offset={}, check it later checkImmunity={}, born={}", i,
                                    checkImmunityTime, new Date(msgExt.getBornTimestamp()));
                                break;//退出回查
                            }
                        }
                        List<MessageExt> opMsg = pullResult.getMsgFoundList();//op half msg
                        
                        boolean isNeedCheck = (opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime)//消息未被删除且消息存储时间距离当前超过了60s
                            || (opMsg != null && (opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout))//判断当前获取的最后一条OpMsg的存储时间是否超过了事务超时时间,如果为true也要进行事务状态回查,为什么要这么做呢?
                            || (valueOfCurrentMinusBorn <= -1);

                        if (isNeedCheck) {//需要回查
                            if (!putBackHalfMsgQueue(msgExt, i)) {//如果需要发送事务状态回查消息,则先将消息再次发送到HALF_TOPIC主题中,发送成功则返回true,否则返回false, 如果发送成功,会将该消息的queueOffset、commitLogOffset设置为重新存入的偏移量
                                continue;
                            }
                            listener.resolveHalfMsg(msgExt);//异步向producer发送CHECK_TRANSACTION_STATE命令查询producer本地事务状态,此时broker作为client端,producer作为服务端
                        } else {
                        	//如果无法判断是否发送回查消息,则加载更多的op(已处理)消息进行筛选
                            pullResult = fillOpRemoveMap(removeMap, opQueue, pullResult.getNextBeginOffset(), halfOffset, doneOpOffset);
                            log.info("The miss offset:{} in messageQueue:{} need to get more opMsg, result is:{}", i,
                                messageQueue, pullResult);
                            continue;
                        }
                    }//end else
                    newOffset = i + 1;
                    i++;
                }//end while
                if (newOffset != halfOffset) {
                    /*
                     *	 保存(Prepare)消息队列的回查进度。保存到ConsumerOffsetManager.offsetTable,key是RMQ_SYS_TRACE_TOPIC@CID_RMQ_SYS_TRANS,
                     * 	跟普通消息的topic@groupname不同,half和op half消息消息没有使用真实的groupname,而是重新定义了系统groupname==CID_RMQ_SYS_TRANS         
                     */
                	transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset);
                }
                long newOpOffset = calculateOpOffset(doneOpOffset, opOffset);
                if (newOpOffset != opOffset) {
                	//保存处理队列(op)的进度。保存到ConsumerOffsetManager.offsetTable,key是RMQ_SYS_TRANS_OP_HALF_TOPIC@CID_RMQ_SYS_TRANS
                    transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset);
                }
            }//end for
        } catch (Exception e) {
            e.printStackTrace();
            log.error("Check error", e);
        }

    }

该方法核心功能就是判断prepare消息是否在op half内,如果不在,说明prepare消息未被commit/rollback处理过,需要发起回查,如果在,则不需要发起回查。里面的fillOpRemoveMap方法难理解,下面看该方法代码和注释

/**
     * Read op message, parse op message, and fill removeMap
     *
     * @param removeMap Half message to be remove, key:halfOffset, value: opOffset.
     * @param opQueue Op message queue.
     * @param pullOffsetOfOp The begin offset of op message queue.
     * @param miniOffset The current minimum offset of half message queue.
     * @param doneOpOffset Stored op messages that have been processed.
     * @return Op message result.
     */
    /*
     * 	传入参数解释:
     * removeMap:处理过的prepare消息保存到该集合,key:halfqueueOffset, value: opqueueOffset.
     * opQueue: op half queue
     * pullOffsetOfOp: op half queue上当前queueoffset
     * miniOffset:	half消息队列上当前queueoffset。不要被英文注释给蒙蔽了,不是最小offset,而是当前half上的queueoffset
     * doneOpOffset: 已经被处理过的op half消息的queueuoffset保存到该集合
     * 
     *	 功能:具体实现逻辑是从op half主题消息队列中拉取32条,如果拉取的消息队列偏移量大于等于half toic消息队列的当前queueoffset时,会添加到removeMap中,表示已处理过。
     * removeMap里存放prepare消息队列中已经commit或者rollback的偏移量和待操作队列的消息偏移量(发送commit或rollback后,会往待操作队列中写)
     * doneOpOffset存放待操作队列的消息偏移量
     * 
     */
    private PullResult fillOpRemoveMap(HashMap<Long, Long> removeMap,
        MessageQueue opQueue, long pullOffsetOfOp, long miniOffset, List<Long> doneOpOffset) {
        PullResult pullResult = pullOpMsg(opQueue, pullOffsetOfOp, 32);//从commitlog上拉取pullOffsetOfOp位置开始OP HALF主题消息队列下的32条消息
        if (null == pullResult) {
            return null;
        }
        if (pullResult.getPullStatus() == PullStatus.OFFSET_ILLEGAL
            || pullResult.getPullStatus() == PullStatus.NO_MATCHED_MSG) {//go
            log.warn("The miss op offset={} in queue={} is illegal, pullResult={}", pullOffsetOfOp, opQueue,
                pullResult);
            //offset非法or没有匹配的msg,说明需要更新op half的offset啦
            transactionalMessageBridge.updateConsumeOffset(opQueue, pullResult.getNextBeginOffset());//更新op half topic的queueOffset到ConsumerOffsetManager.offsetTable,注意key是RMQ_SYS_TRANS_OP_HALF_TOPIC@CID_RMQ_SYS_TRANS
            return pullResult;
        } else if (pullResult.getPullStatus() == PullStatus.NO_NEW_MSG) {
            log.warn("The miss op offset={} in queue={} is NO_NEW_MSG, pullResult={}", pullOffsetOfOp, opQueue,
                pullResult);
          //该pullOffsetOfOp位置后没有消息,说明不需要更新op half的offset
            return pullResult;
        }
        List<MessageExt> opMsg = pullResult.getMsgFoundList();//拉取到的op half下的消息集合
        if (opMsg == null) {
            log.warn("The miss op offset={} in queue={} is empty, pullResult={}", pullOffsetOfOp, opQueue, pullResult);
            return pullResult;
        }
        for (MessageExt opMessageExt : opMsg) {//遍历拉取到的op half topic队列的消息集合
            /*
             * 	对于op half队列内保存的消息来说
             *	 消息的body是prepare消息在consumequeu上的queueOffset
             *	 消息的tag是TransactionalMessageUtil.REMOVETAG
             *	 在TransactionalMessageBridge.addRemoveTagInTransactionOp(MessageExt, MessageQueue)做的
             * queueOffset变量就是prepare消息在consumequeue上的offset
             */
        	Long queueOffset = getLong(new String(opMessageExt.getBody(), TransactionalMessageUtil.charset));//获取op half消息的body,即prepare消息在cq上的queueOffset。
            log.info("Topic: {} tags: {}, OpOffset: {}, HalfOffset: {}", opMessageExt.getTopic(),
                opMessageExt.getTags(), opMessageExt.getQueueOffset(), queueOffset);
            if (TransactionalMessageUtil.REMOVETAG.equals(opMessageExt.getTags())) {//true,op消息的tag就是TransactionalMessageUtil.REMOVETAG
                if (queueOffset < miniOffset) {
                	/*
                	 * 	op half消息的body存储的是对应的prepare的queueoffset,这点首先要明白
                	 * 	事务消息的流程是先发prepare消息到broker(消息存储到commitlog,topic是half),接着执行producer端本地db事务,事务执行后发送commit/rollback/unknow消息到broker,
                	 * 	无论是commit/rollback,都会在op half保存一条消息,该消息存在,说明对应的prepare消息就是被删除了。那么从op half拉取出来的消息都是需要进行回查的了,这么理解没错,但是每次都回查
                	 * 	那么多,是否可以进行下过滤,过滤掉已经被回查过的呢?因此就有doneOpOffset,当op half消息对应的prepare消息queueoffset小于当前half消息的queueoffset,说明该prepare消息
                	 * 	已经被(处理过且)回查过了,因此无需再进行回查,保存到doneOpOffset。
                	 */
                    doneOpOffset.add(opMessageExt.getQueueOffset());
                } else {
                	//把已经被commit/rollback处理过的消息保存到removeMap
                    removeMap.put(queueOffset, opMessageExt.getQueueOffset());
                }
            } else {
                log.error("Found a illegal tag in opMessageExt= {} ", opMessageExt);
            }
        }//for end
        log.debug("Remove map: {}", removeMap);
        log.debug("Done op list: {}", doneOpOffset);
        return pullResult;
    }

在op half上的消息都是被commit/rollback处理过的消息,那么都保存到removeMap,为什么还要有doneOpOffset呢?是为了减少消息的判断,为了过滤,如果op half对应的prepare消息,那说明prepare不仅被处理过了,而且被回查过了,不再需要参与判断了。这个理解有些难,参考https://itzones.cn/2019/07/09/RocketMQ事务消息/ ,该文章有图很能说明:

接着看org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener.resolveHalfMsg(MessageExt),该方法异步发送命令CHECK_TRANSACTION_STATE到producer查询事务状态,对应的producer处理器是ClientRemotingProcessor,最终由TransactionMQProducer.checkExecutor线程池执行task,查询事务的状态,总体流程如下图。

producer端流程和代码比较简单,需要TransactionMQProducer设置线程池处理接收、事务监听器TransactionListener处理回查事务状态,开发人员需要自己实现事务监听器来回查事务执行状态。

有个点需要注意,在broker发producer进行回查的方法org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener.sendCheckMessage(MessageExt)内,并不一定实际是发送给prepare消息的生产的那个producer(具体代码是Channel channel = brokerController.getProducerManager().getAvaliableChannel(groupId);),它是通过broker保存的producerGroup内选择一个producer进行回查(通常producer也是一组集群),因此producer端事务状态和transactionId需要保存在db or redis等,这样才可以被同组内的其它producer查询到事务状态。

还有个点需要注意,对于prepare消息是发送到了broker1,那么commit消息也是要发broker1才行,在org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.endTransaction(SendResult, LocalTransactionState, Throwable)方法内的final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName());设置的,而sendResult又是prepare消息的发送结果,因此保证了commit/rollback消息都是发的同一台broker,不然broker无法回查了。需要注意的是prepare默认发送失败的情况下跟普通消息一样最大重发3次,但是commit/rollback只发送一次,发送失败了,只能由broker回查决定了。

producer发送的commit消息就属于正常待消费的消息了,客户端可以选择顺序/并发消费都行,消费和普通消息消费没有不同。

 

考虑几个事务消息的异常状态:

1.preprare消息发送成功,本地事务执行成功,但是producer宕机

该情况broker会进行回查事务状态,从而提交事务,发送消息给下游系统。

2.preprare消息发送成功,本地事务执行过程中producer宕机了

事务执行过程宕机了,那么数据库自动会回滚事务,事务就是没执行成功,因此broker回查从而删除preprae消息。

3.preprare消息发送成功,本地事务执行成功,但是发送commit消息给broker失败(发送给prepare消息接收的那台broker),因为broker宕机?

启动该broker,broker回查到事务执行成功,从而提交消息,发送消息给下游系统进行消费,该情况会导致下游有长时间延迟才收到消息消费。

4.客户端消费消息失败了,怎么办?

rocketmq给出的方案是人工解决,这样的情况不能多,如果多了,需要优化业务和代码,实际用rocketmq的事务消息,客户端消费失败情况是少的,比如扣库存动作,基本都是成功的。客户端消费失败的情况通常是通过对账根据业务情况解决。

 

来段网上总结的话,自己对这种文字性总结总是说的不好,感觉自己说的比较大白话,别人说的更加专业:

使用rocketmq来保证分布式事务属于消息一致性方案,通过消息中间件保证上、下游应用数据操作的一致性。基本思路是将本地操作和发送消息放在一个事务中,保证本地操作和消息发送要么两者都成功或者都失败。下游应用向消息系统订阅该消息,收到消息后执行相应操作。
消息方案从本质上讲是将分布式事务转换为两个本地事务,然后依靠下游业务的重试机制达到最终一致性。基于消息的最终一致性方案对应用侵入性也很高,应用需要进行大量业务改造,成本较高。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章