深入探究 RocketMQ 中的 Broker2Client 组件

在分布式消息队列系统 RocketMQ 中,Broker2Client 组件扮演着至关重要的角色,它负责协调消息代理(Broker)与客户端(Client)之间的交互,确保消息能够高效、可靠地在两者之间传递。本文将深入探讨 Broker2Client 组件的主要属性和方法,帮助开发者更好地理解其工作机制和应用场景。

一、Broker2Client 组件概述​

Broker2Client 并非一个独立的物理组件,而是一系列功能和逻辑的集合,它涵盖了从 Broker 端到 Client 端通信的各个方面,主要讲一些消息(生产者事务,通知消费者Ids的变化,消费者复位偏移量 查询消费者状态)进行推送到Client上。

二、主要方法​

1.checkProducerTransactionState

在 RocketMQ 事务消息处理流程里,checkProducerTransactionState方法至关重要。它主要用于 Broker 对生产者事务状态的检查与确认。下面为你详细介绍:

  1. 方法定义与调用时机 :当 Broker 端开启事务消息支持时,在事务消息处理过程中,若 Broker 长时间未收到生产者关于事务状态的明确反馈(比如生产者在发送半消息并执行本地事务后,因网络等原因未能及时告知 Broker 事务最终状态),Broker 会定期调用checkProducerTransactionState方法来查询生产者事务状态。这个定期检查机制确保了事务消息状态的最终一致性,防止事务消息长时间处于不确定状态。
  2. 核心功能:该方法内部逻辑主要是向生产者发起事务状态查询请求。它会携带事务消息相关标识,如全局唯一的事务 ID 等关键信息。生产者接收到此查询请求后,会根据事务 ID 在本地事务记录中查找对应的事务状态,然后将事务执行结果(提交、回滚或未知等状态)反馈给 Broker。例如,若生产者本地事务已成功提交,在收到 Broker 的状态查询时,会告知 Broker 事务已提交,Broker 便可以将对应的半消息标记为可投递状态,从而让消费者能够消费该消息;若事务回滚,Broker 则会丢弃这条半消息。
  3. 对系统的意义 :在分布式事务场景下,checkProducerTransactionState方法保障了 RocketMQ 事务消息的可靠性。通过不断检查生产者事务状态,即使面对复杂的网络环境或生产者故障恢复等情况,也能确保事务消息的最终状态正确确定,进而保证了整个分布式系统中数据的一致性。例如在电商订单创建与库存扣减的分布式事务场景中,若订单服务(生产者)在发送订单创建消息(事务消息)并执行本地订单创建事务后出现短暂故障,Broker 通过checkProducerTransactionState方法能够在生产者恢复后获取准确的事务状态,若订单创建成功,库存服务(消费者)就能及时消费消息并扣减库存,保证业务流程的完整性和数据一致性

代码示例:

java 复制代码
/**
     * 检查生产者的事务状态 跟事务消息配合起来来进行使用
     * @param group 生产者组
     * @param channel 生产者网络的连接
     * @param requestHeader 检查事务消息请求的头
     * @param messageExt  消息的扩展数据
     * @throws Exception 异常
     */
    public void checkProducerTransactionState(
        final String group,
        final Channel channel,
        final CheckTransactionStateRequestHeader requestHeader,
        final MessageExt messageExt) throws Exception {
        //构建请求信息对象
        RemotingCommand request =
            RemotingCommand.createRequestCommand(RequestCode.CHECK_TRANSACTION_STATE, requestHeader);
        request.setBody(MessageDecoder.encode(messageExt, false));
        try {
            this.brokerController.getRemotingServer().invokeOneway(channel, request, 10);
        } catch (Exception e) {
            log.error("Check transaction failed because invoke producer exception. group={}, msgId={}, error={}",
                    group, messageExt.getMsgId(), e.toString());
        }
    }

CheckTransactionStateRequestHeader对象信息

java 复制代码
public class CheckTransactionStateRequestHeader extends RpcRequestHeader {
    /**
     * 事务状态表的偏移量
     */
    @CFNotNull
    private Long tranStateTableOffset;
    /**
     * commitLog偏移量
     */
    @CFNotNull
    private Long commitLogOffset;
    /**
     * 消息id
     */
    private String msgId;
    /**
     * 事务id
     */
    private String transactionId;
    /**
     * 偏移量消息的id
     */
    private String offsetMsgId;

}

2.notifyConsumerIdsChanged

notifyConsumerIdsChanged方法是维系消费者相关状态一致性与及时通信的关键一环。它主要用于在消费者 ID 发生变更时,通知相关的客户端,以便客户端能够及时调整自身状态,确保消息消费的连续性与准确性。

方法触发时机

当 Broker 端检测到消费者组内的消费者 ID 列表发生变化时,notifyConsumerIdsChanged方法会被触发。这种变化可能源于多种情况,比如新的消费者实例加入到某个消费者组,以提升消息消费的并行度与整体吞吐量;或者某个正在运行的消费者实例因故障、维护等原因从消费者组中退出。在这些场景下,Broker 需要将消费者 ID 的变更情况及时告知其他相关客户端,而notifyConsumerIdsChanged方法就承担了这一重要职责。

代码:

java 复制代码
  //消费者ids有变化之后 会进行推送给客户端一个事件的通知
    public void notifyConsumerIdsChanged(
        final Channel channel,
        final String consumerGroup) {
        if (null == consumerGroup) {
            log.error("notifyConsumerIdsChanged consumerGroup is null");
            return;
        }
        //构建者消费者ids变化的请求头
        NotifyConsumerIdsChangedRequestHeader requestHeader = new NotifyConsumerIdsChangedRequestHeader();
        requestHeader.setConsumerGroup(consumerGroup);
        //创建好request
        RemotingCommand request =
            RemotingCommand.createRequestCommand(RequestCode.NOTIFY_CONSUMER_IDS_CHANGED, requestHeader);

        try {
            this.brokerController.getRemotingServer().invokeOneway(channel, request, 10);
        } catch (Exception e) {
            log.error("notifyConsumerIdsChanged exception. group={}, error={}", consumerGroup, e.toString());
        }
    }

3.resetOffset

在 RocketMQ 的 Broker2Client 组件中,resetOffset方法在消息消费的精准控制与异常恢复场景里意义重大。以下从它的功能用途、执行逻辑以及实际应用场景展开详细介绍:

  1. 功能用途resetOffset方法主要用于重置消费者在特定队列上的消费偏移量。消费偏移量记录了消费者当前在消息队列中消费到的位置,而通过resetOffset方法,能够改变这个位置,让消费者从指定的新位置重新开始消费消息。这在一些特殊情况下,比如消费者需要重新消费历史消息,或者在消费过程中出现偏移量错误需要纠正时,显得尤为重要。
  2. 执行逻辑 :当调用resetOffset方法时,通常需要传入消费者组名称、对应的消息队列对象以及期望重置到的目标偏移量等参数。Broker2Client 会首先验证传入参数的合法性,例如检查消费者组是否存在,对应的消息队列是否有效等。验证通过后,它会在内部维护的消费者偏移量存储结构中(可能是内存中的数据结构或者关联的持久化存储,用于记录消费者组在各个队列上的消费偏移量),找到与该消费者组和消息队列对应的记录,并将其更新为新传入的目标偏移量。在后续的消息拉取过程中,消费者会依据这个被重置后的偏移量从消息队列中获取消息。
  3. 实际应用场景 :假设在一个实时数据分析系统中,消费者负责从 RocketMQ 的队列里读取业务数据进行分析。但由于某些原因,部分数据的分析结果出现错误,需要重新消费这些数据进行重新分析。此时就可以利用resetOffset方法,将消费者在对应队列上的偏移量重置到错误数据起始位置的前一个位置,使得消费者能够重新获取并处理这些数据。又或者在系统升级过程中,由于一些配置变更导致消费者的偏移量记录出现混乱,通过resetOffset方法纠正偏移量,能让消费者恢复到正确的消费进度,保证系统正常运行。

代码:

java 复制代码
 /**
     * 复位偏移量
     * @param topic topic
     * @param group 消费组
     * @param timeStamp 时间戳 -1 就是查询最大的偏移量
     * @param isForce 是否强制
     * @param isC 是否是C语言的标识
     * @return
     *
     * broker收到了一个请求,返回一个响应回去
     */
    public RemotingCommand resetOffset(String topic, String group, long timeStamp, boolean isForce,
                                       boolean isC) {

        final RemotingCommand response = RemotingCommand.createResponseCommand(null);

        //获取topic的元数据
        TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic);
        //topic元数据为空 报错
        if (null == topicConfig) {
            log.error("[reset-offset] reset offset failed, no topic in this broker. topic={}", topic);
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark("[reset-offset] reset offset failed, no topic in this broker. topic=" + topic);
            return response;
        }

        Map<MessageQueue, Long> offsetTable = new HashMap<MessageQueue, Long>();
        //遍历topic在当前broker上的写队列的数量
        for (int i = 0; i < topicConfig.getWriteQueueNums(); i++) {
            MessageQueue mq = new MessageQueue();
            mq.setBrokerName(this.brokerController.getBrokerConfig().getBrokerName());
            mq.setTopic(topic);
            mq.setQueueId(i);

            //i是queueId 查询消费组对一个topic的一个queue的偏移量
            long consumerOffset =
                this.brokerController.getConsumerOffsetManager().queryOffset(group, topic, i);
            if (-1 == consumerOffset) {
                response.setCode(ResponseCode.SYSTEM_ERROR);
                response.setRemark(String.format("THe consumer group <%s> not exist", group));
                return response;
            }

            long timeStampOffset;
            if (timeStamp == -1) {
                //查询最大的偏移量
                timeStampOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, i);
            } else {
                //查询时间戳对应的偏移量
                timeStampOffset = this.brokerController.getMessageStore().getOffsetInQueueByTime(topic, i, timeStamp);
            }

            if (timeStampOffset < 0) {
                log.warn("reset offset is invalid. topic={}, queueId={}, timeStampOffset={}", topic, i, timeStampOffset);
                timeStampOffset = 0;
            }
            //是否强制启用 或者时间戳的偏移量小于消费组的偏移量
            //小于的话 就使用时间戳的偏移量,如果大于的话 就使用消费组的偏移量
            if (isForce || timeStampOffset < consumerOffset) {
                offsetTable.put(mq, timeStampOffset);
            } else {
                offsetTable.put(mq, consumerOffset);
            }
        }
        //构建一个复位偏移量的请求头
        ResetOffsetRequestHeader requestHeader = new ResetOffsetRequestHeader();
        requestHeader.setTopic(topic);
        requestHeader.setGroup(group);
        requestHeader.setTimestamp(timeStamp);

        RemotingCommand request =
            RemotingCommand.createRequestCommand(RequestCode.RESET_CONSUMER_CLIENT_OFFSET, requestHeader);
       //是否是C语言的标识
        if (isC) {
            // c++ language
            ResetOffsetBodyForC body = new ResetOffsetBodyForC();
            List<MessageQueueForC> offsetList = convertOffsetTable2OffsetList(offsetTable);
            body.setOffsetTable(offsetList);
            request.setBody(body.encode());
        } else {
            // other language
            ResetOffsetBody body = new ResetOffsetBody();
            body.setOffsetTable(offsetTable);
            request.setBody(body.encode());
        }

        //获取消费组的信息
        ConsumerGroupInfo consumerGroupInfo =
            this.brokerController.getConsumerManager().getConsumerGroupInfo(group);

        //遍历消费组的信息 对消费组的每个消费者都推送一个复位偏移量的请求
        if (consumerGroupInfo != null && !consumerGroupInfo.getAllChannel().isEmpty()) {
            ConcurrentMap<Channel, ClientChannelInfo> channelInfoTable =
                consumerGroupInfo.getChannelInfoTable();
            for (Map.Entry<Channel, ClientChannelInfo> entry : channelInfoTable.entrySet()) {
                int version = entry.getValue().getVersion();
                if (version >= MQVersion.Version.V3_0_7_SNAPSHOT.ordinal()) {
                    try {
                        this.brokerController.getRemotingServer().invokeOneway(entry.getKey(), request, 5000);
                        log.info("[reset-offset] reset offset success. topic={}, group={}, clientId={}",
                            topic, group, entry.getValue().getClientId());
                    } catch (Exception e) {
                        log.error("[reset-offset] reset offset exception. topic={}, group={} ,error={}",
                            topic, group, e.toString());
                    }
                } else {
                    response.setCode(ResponseCode.SYSTEM_ERROR);
                    response.setRemark("the client does not support this feature. version="
                        + MQVersion.getVersionDesc(version));
                    log.warn("[reset-offset] the client does not support this feature. channel={}, version={}",
                        RemotingHelper.parseChannelRemoteAddr(entry.getKey()), MQVersion.getVersionDesc(version));
                    return response;
                }
            }
        } else {
            String errorInfo =
                String.format("Consumer not online, so can not reset offset, Group: %s Topic: %s Timestamp: %d",
                    requestHeader.getGroup(),
                    requestHeader.getTopic(),
                    requestHeader.getTimestamp());
            log.error(errorInfo);
            response.setCode(ResponseCode.CONSUMER_NOT_ONLINE);
            response.setRemark(errorInfo);
            return response;
        }
        response.setCode(ResponseCode.SUCCESS);
        ResetOffsetBody resBody = new ResetOffsetBody();
        resBody.setOffsetTable(offsetTable);
        response.setBody(resBody.encode());
        return response;
    }

4.getConsumeStatus

在 RocketMQ 中,Broker2Client组件里的getConsumeStatus方法在消息消费状态管理方面有着关键作用。

方法功能

该方法主要用于获取消费者对特定消息队列的消费状态。在 RocketMQ 的消费模型中,消费者通过从 Broker 拉取消息进行处理。而getConsumeStatus方法能让 Broker 快速了解某个消费者组下的消费者在特定消息队列上的消费进度,比如是否已经落后太多消息未处理,或者消费是否正常进行等。这对于 Broker 监控整个消息消费流程的健康状况以及进行相应的调度和优化十分重要。

方法调用时机

  1. 定期监控 :Broker 通常会在内部以一定时间间隔调用getConsumeStatus方法,来周期性地检查各个消费者组对不同消息队列的消费状态。通过持续的状态获取,Broker 可以及时发现消费延迟等异常情况。例如,在电商系统中,如果订单支付成功消息的消费出现延迟,可能导致库存不能及时更新,通过定期调用该方法就能及时察觉并采取措施。

  2. 消费者状态变更时 :当消费者的状态发生变化,如消费者启动、停止或者消费者组内成员发生变动时,Broker 也可能调用getConsumeStatus方法,来获取最新的消费状态信息,以便重新进行负载均衡等操作。比如,当有新的消费者加入消费者组时,Broker 需要了解新老消费者对各个消息队列的消费进度,从而合理分配消息队列给新成员,确保消费的高效性和均衡性。

代码:

java 复制代码
//获取消费者状态 针对某一个消费者查询它的消费状态
    public RemotingCommand getConsumeStatus(String topic, String group, String originClientId) {
        final RemotingCommand result = RemotingCommand.createResponseCommand(null);

        //构建消费状态的请求头
        GetConsumerStatusRequestHeader requestHeader = new GetConsumerStatusRequestHeader();
        requestHeader.setTopic(topic);
        requestHeader.setGroup(group);
        RemotingCommand request =
            RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_STATUS_FROM_CLIENT,
                requestHeader);

        //这个Map key为clientId(consumer客户端的id),value为MessageQueue,key为MessageQueue,value为偏移量
        Map<String, Map<MessageQueue, Long>> consumerStatusTable =
            new HashMap<String, Map<MessageQueue, Long>>();
        //获取到一个消费者组里的所有的消费者信息
        ConcurrentMap<Channel, ClientChannelInfo> channelInfoTable =
            this.brokerController.getConsumerManager().getConsumerGroupInfo(group).getChannelInfoTable();
        //为空的处理逻辑
        if (null == channelInfoTable || channelInfoTable.isEmpty()) {
            result.setCode(ResponseCode.SYSTEM_ERROR);
            result.setRemark(String.format("No Any Consumer online in the consumer group: [%s]", group));
            return result;
        }

        //遍历往consumerStatusTable中进行设置值
        for (Map.Entry<Channel, ClientChannelInfo> entry : channelInfoTable.entrySet()) {
            int version = entry.getValue().getVersion();
            String clientId = entry.getValue().getClientId();
            if (version < MQVersion.Version.V3_0_7_SNAPSHOT.ordinal()) {
                result.setCode(ResponseCode.SYSTEM_ERROR);
                result.setRemark("the client does not support this feature. version="
                    + MQVersion.getVersionDesc(version));
                log.warn("[get-consumer-status] the client does not support this feature. channel={}, version={}",
                    RemotingHelper.parseChannelRemoteAddr(entry.getKey()), MQVersion.getVersionDesc(version));
                return result;
            } else if (UtilAll.isBlank(originClientId) || originClientId.equals(clientId)) {
                try {
                    //同步发送请求 获取消费者的消费状态 在这里进行同步等待 拿到结果之后给设置到consumerStatusTable里
                    RemotingCommand response =
                        this.brokerController.getRemotingServer().invokeSync(entry.getKey(), request, 5000);
                    assert response != null;
                    switch (response.getCode()) {
                        case ResponseCode.SUCCESS: {
                            if (response.getBody() != null) {
                                GetConsumerStatusBody body =
                                    GetConsumerStatusBody.decode(response.getBody(),
                                        GetConsumerStatusBody.class);
                                //结果设置到consumerStatusTable中
                                consumerStatusTable.put(clientId, body.getMessageQueueTable());
                                log.info(
                                    "[get-consumer-status] get consumer status success. topic={}, group={}, channelRemoteAddr={}",
                                    topic, group, clientId);
                            }
                        }
                        default:
                            break;
                    }
                } catch (Exception e) {
                    log.error(
                        "[get-consumer-status] get consumer status exception. topic={}, group={}, error={}",
                        topic, group, e.toString());
                }

                if (!UtilAll.isBlank(originClientId) && originClientId.equals(clientId)) {
                    break;
                }
            }
        }

        result.setCode(ResponseCode.SUCCESS);
        GetConsumerStatusBody resBody = new GetConsumerStatusBody();
        resBody.setConsumerTable(consumerStatusTable);
        result.setBody(resBody.encode());
        return result;
    }

三 总结

Broker2Client 组件的存在,使得 RocketMQ 的 Broker 与 Client 之间的交互变得有序、高效。在大规模分布式系统中,众多的客户端需要与 Broker 进行频繁的通信。深入理解 Broker2Client 组件的属性和方法,对于开发者优化 RocketMQ 的使用、排查问题以及构建高性能的分布式消息驱动系统具有重要意义。随着 RocketMQ 在各个领域的广泛应用,对这一核心组件的掌握将成为开发者提升技术能力的关键一环。

相关推荐
煤烦恼17 分钟前
scala类与集合
java·大数据·开发语言·人工智能·scala
落榜程序员1 小时前
Java 基础-32-枚举-枚举的应用场景
java·开发语言
晓13131 小时前
第九章Python语言高阶加强-面向对象篇
java·开发语言
快来卷java2 小时前
JVM虚拟机篇(五):深入理解Java类加载器与类加载机制
java·jvm·mysql
禾小西4 小时前
Java 逐梦力扣之旅_[204. 计数质数]
java·算法·leetcode
ゞ 正在缓冲99%…4 小时前
leetcode295.数据流的中位数
java·数据结构·算法·leetcode·
有梦想的攻城狮6 小时前
spring-cloud-alibaba-nacos-config使用说明
java·spring·nacos·springcloud·配置中心
Yan-英杰7 小时前
【百日精通JAVA | SQL篇 | 第三篇】 MYSQL增删改查
java·数据库·sql
矛取矛求9 小时前
C++ 标准库参考手册深度解析
java·开发语言·c++
cijiancao9 小时前
23 种设计模式中的解释器模式
java·设计模式·解释器模式