RocketMQ源码系列(11) — 消息生产者

生产者

我们可以从测试用例 org.apache.rocketmq.example.quickstart.Producer 来看生产者发送消息的流程。

消息生产者发送消息比较简单,发送消息的组件就是 DefaultMQProducer,它在创建时至少需要指定一个生产者组名称,然后设置 NameServer 的地址,这样才能从 NameServer 拉取到 Broker 和 Topic 等信息。

接着就是创建 Message 对象,指定了消息所属 topic,标签,以及消息体,然后调用 DefaultMQProducer 将消息发送到 Broker 去。

java 复制代码
public class Producer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        // 创建消息生产者,并指定生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // 设置xNameServer地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        // 启动生产者
        producer.start();
        // 创建消息
        byte[] body = ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET);
        Message msg = new Message("TopicTest", "TagA", body);
        // 发送消息
        SendResult sendResult = producer.send(msg);
        System.out.printf("Results: %s%n", sendResult);
        // 关闭生产者
        producer.shutdown();
    }
}

消息生产者

1、DefaultMQProducer

消息生产者 DefaultMQProducer 继承自 ClientConfig,实现了 MQProducer 接口,MQProducer 又是继承自 MQAdmin 接口。MQAdmin 提供了创建topic、查询消息、查询偏移量等API;MQProducer 则主要是发送消息相关API。

从 DefaultMQProducer 的构造方法知道,一个消息生产者是归属于一个命名空间(namespace)下的一个组(producerGroup),也就是说我们在使用RocketMQ时,可以通过命名空间和组对消息进行一个分类。而发送消息的真正实现组件是 DefaultMQProducerImpl,DefaultMQProducer 启动时会启动这个组件。

从 DefaultMQProducer 的属性可以了解到:

  • 创建topic时,默认的读写队列数量是4;
  • 发送消息的超时时间是30秒;
  • 发送消息最大大小是4MB,消息超过4KB后就进行压缩;
  • 支持消息发送失败后重试,也支持重试其它broker。
java 复制代码
public class DefaultMQProducer extends ClientConfig implements MQProducer {
    // 生产者实现
    protected final transient DefaultMQProducerImpl defaultMQProducerImpl;
    // 生产组
    private String producerGroup;
    // topic 队列默认数量
    private volatile int defaultTopicQueueNums = 4;
    // 发送消息超时时间 30 秒
    private int sendMsgTimeout = 30000;
    // 消息压缩阈值,超过 4KB 就压缩数据
    private int compressMsgBodyOverHowmuch = 1024 * 4;
    // 同步模式下,发送消息失败后重试次数
    private int retryTimesWhenSendFailed = 2;
    // 异步模式下,发送消息失败后重试次数
    private int retryTimesWhenSendAsyncFailed = 2;
    // 发送消息失败后是否重试其它的Broker
    private boolean retryAnotherBrokerWhenNotStoreOK = false;
    // 消息最大大小
    private int maxMessageSize = 1024 * 1024 * 4; // 4M

    public DefaultMQProducer(final String namespace, // 消息生产者命名空间
                             final String producerGroup,  // 消息生产组所属组
                             RPCHook rpcHook // 钩子函数) {
        this.namespace = namespace;
        this.producerGroup = producerGroup;
        defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
    }
}
// 发送消息
public SendResult send(Message msg) throws Exception {
    msg.setTopic(withNamespace(msg.getTopic())); // 设置命名空间
    return this.defaultMQProducerImpl.send(msg); // 发送消息
}

2、DefaultMQProducerImpl

DefaultMQProducerImpl 是发送消息的实现类,发送消息首先要确定向哪个Broker发送,所以它用一个topic路由表 topicPublishInfoTable 来维护topic与broker的路由关系。

而真正在与broker进行网络调用的组件则是 MQClientInstance,它是客户端与Broker进行通信的组件。生产者客户端启动完成之后,会向其注册生产者对象,然后向所有broker发送心跳,上报自己的元数据。

java 复制代码
public class DefaultMQProducerImpl implements MQProducerInner {
    // topic 发布信息映射表
    private final ConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable =  new ConcurrentHashMap<>();
    // MQ 网络客户端实例
    private MQClientInstance mQClientFactory;
    // 消息发送容错策略
    private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy();
    // 压缩器
    private final Compressor compressor = CompressorFactory.getCompressor(compressType);
    
    public void start(final boolean startFactory) throws MQClientException {
        switch (this.serviceState) {
            case CREATE_JUST:
                this.serviceState = ServiceState.START_FAILED;
                // 创建 MQClientInstance,同一个客户端只会有一个,MQClientManager 用一个 factoryTable 表来存
                this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
                // 注册生产组,将 生产者放入 producerTable
                boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
                // topic 发布信息,一个新建的 TopicPublishInfo
                this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());
                // 启动MQClient
                if (startFactory) {
                    mQClientFactory.start();
                }
                this.serviceState = ServiceState.RUNNING;
                break;
            ...
        }
        // 客户端向所有broker发送心跳,保持与broker的连接
        this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
    }
}

3、MQClientInstance

MQClientInstance 正常情况下一个客户端只会有一个实例,从它的结构中可以大概了解到,MQClientInstance 就是生产者和消费者的客户端实例,它负责管理生产者、消费者组件,它会定时去更新路由数据,Broker地址表等基础信息,还负责消息的拉取、重平衡等。

MQClientInstance 中有一个组件 MQClientAPIImpl,它是最后一步真正调用Broker的组件,其内部是基于 NettyRemotingClient 实现的远程调用与Broker通信,这个组件前面有介绍过。

java 复制代码
public class MQClientInstance {
    // 客户端配置
    private final ClientConfig clientConfig;
    // 客户端ID
    private final String clientId;

    // producer 组件映射表
    private final ConcurrentMap<String/* group */, MQProducerInner> producerTable = new ConcurrentHashMap<>();
    // consumer 组件映射表
    private final ConcurrentMap<String/* group */, MQConsumerInner> consumerTable = new ConcurrentHashMap<>();
    // admin 组件映射表
    private final ConcurrentMap<String/* group */, MQAdminExtInner> adminExtTable = new ConcurrentHashMap<>();

    // netty config
    private final NettyClientConfig nettyClientConfig;
    // 客户端通信API实现组件
    private final MQClientAPIImpl mQClientAPIImpl;
    // 管理客户端网络通信组件
    private final MQAdminImpl mQAdminImpl;
    // 服务端调用客户端的处理器
    private final ClientRemotingProcessor clientRemotingProcessor;

    // topic 路由数据
    private final ConcurrentMap<String/* Topic */, TopicRouteData> topicRouteTable = new ConcurrentHashMap<>();
    // broker 地址表
    private final ConcurrentMap<String/* Broker Name */, HashMap<Long/* brokerId */, String/* address */>> brokerAddrTable = new ConcurrentHashMap<>();
    // broker 版本表
    private final ConcurrentMap<String/* Broker Name */, HashMap<String/* address */, Integer>> brokerVersionTable = new ConcurrentHashMap<>();
    // 定时任务执行器
    private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {...);
    
    // 拉取消息服务
    private final PullMessageService pullMessageService;
    // 重平衡服务
    private final RebalanceService rebalanceService;

    private final DefaultMQProducer defaultMQProducer;
    // 消费者状态管理器
    private final ConsumerStatsManager consumerStatsManager;
    // 发送心跳总数
    private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0);
}    

Topic路由数据结构

作为消息客户端,首先需要知道每个topic被发送到哪个Broker上,DefaultMQProducerImpl 用一个topic信息表topicPublishInfoTable来维护topic与broker间的关系。

每个 Broker 启动时,会向 NameServer 注册自己的信息,包括自己的地址、Topic信息等等,NameServer 中的路由信息管理组件 RouteInfoManager 就维护了所有的Broker元数据。客户端启动后,就会从 RouteInfoManager 获取所有的Broker元数据到本地,并且 MQClientInstance 中会启动一个定时任务,每隔30秒更新一次topic信息表。

这个topic信息表的结构如下图,一个 topic 会对应一个 TopicPublishInfo,它包含了多个消息队列 MessageQueue 和一个topic路由数据 TopicRouteData。MessageQueue 主要记录了这个 topic 发送到哪些 broker 中的哪些队列中了。TopicRouteData 主要包含多个队列信息 QueueData 和 Broker信息 BrokerData;QueueData 是用来表示这个 topic 在每个 broker 上的配置信息(读写队列数量、权限等),BrokerData 则表示这个 topic 在哪些broker上, 以及对应的 broker 地址表。

从上图这个结构我们可以知道RocketMQ对topic路由的逻辑结构设计,一个 topic 的消息可以发送到多个Broker集群中,可以分布在集群中多组broker上,在一个broker上每个topic还有多个读写队列。

所以 topicPublishInfoTable 就维护了这些关系,在发送消息时,就可以从这个信息表中去根据一定策略(默认是轮询策略)去计算消息发送到哪个集群的哪组broker中的哪个队列中,然后得到broker的地址,就可以将消息发送到这个broker去。

发送消息流程

1、发送消息核心流程

DefaultMQProducerImpl 发送消息的核心流程可以简化成如下几步:

  1. 根据 topic 获取路由信息 TopicPublishInfo
  2. 从 TopicPublishInfo 中以轮询的策略获取一个消息队列 MessageQueue
  3. 将消息发送到消息队列 MessageQueue
  4. 同步模式下,消息发送失败时,在没超时的情况下,默认支持重试2次
java 复制代码
private SendResult sendDefaultImpl(Message msg, CommunicationMode communicationMode, SendCallback sendCallback, long timeout // 超时时间 ) throws Exception {
    // 查询 topic 路由信息
    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    MessageQueue mq = null;
    SendResult sendResult = null;
    // 推送消息支持一定次数的重试,同步模式下,失败重试次数,默认3次
    int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
    int times = 0;
    for (; times < timesTotal; times++) {
        // 要发送的broker
        String lastBrokerName = null == mq ? null : mq.getBrokerName();
        // 轮询选择一个消息队列
        MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
        if (mqSelected != null) {
            mq = mqSelected;
            try {
                // 时间信息,判断是否超时
                beginTimestampPrev = System.currentTimeMillis();
                long costTime = beginTimestampPrev - beginTimestampFirst;
                if (timeout < costTime) {
                    callTimeout = true;
                    break;
                }
                // 发送消息到某个消息队列
                sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                ...
            } catch (Exception e) {...}
        }
    }    
...
}

发送消息时,会根据消息队列中的 brokerName 找到对应的 broker 地址,MQClientInstance 中有一个broker地址表 brokerAddrTable 维护了broker的地址信息。得到broker地址后,再对消息做一些设置,然后将消息根据协议转码成 SendMessageRequestHeader,最后再调用 MQClientInstance 的 MQClientAPIImpl 同步或异步的方式将消息发送到Broker上。

java 复制代码
private SendResult sendKernelImpl(final Message msg, MessageQueue mq, CommunicationMode communicationMode, // 发送模式
    SendCallback sendCallback, TopicPublishInfo topicPublishInfo, long timeout) throws Exception {
    long beginStartTime = System.currentTimeMillis();
    // 找出要发送到哪个 broker 上
    String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
    try {
        // 发送消息时设置消息唯一ID
        // 命名空间设置
        // 消息超过4KB压缩标识
        // 事务消息标识
        ...
        // 发送消息请求头协议
        SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
        requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); // 消息组
        requestHeader.setTopic(msg.getTopic()); // topic
        requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey()); // 默认topic
        requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums()); // 默认队列数 
        requestHeader.setQueueId(mq.getQueueId()); // 发送到哪个队列
        requestHeader.setSysFlag(sysFlag); // 消息系统标识
        ...

        SendResult sendResult = null;
        switch (communicationMode) {
            case ASYNC: // 同步发送
                // 异步发送消息
                sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(brokerAddr, ..., msg, sendCallback, ...);
                break;
            case ONEWAY:
            case SYNC:
                // 同步发送消息
                sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(brokerAddr, ... msg);
                break;
        }
        // 发送消息钩子函数
        ...
        return sendResult;
    } catch (RemotingException e) {...}
}

2、消息发送处理器

通过消息发送编码可以找到 Broker 端对应的消息处理器是 SendMessageProcessor

将 SendMessageProcessor 对发送消息的处理逻辑简化下,它首先会对消息做一个预发送处理,再对消息检查,如果检查不通过就直接返回失败;检查通过,则根据协议将消息转成内部的消息结构 MessageExtBrokerInner。

如果是事务消息,则将消息提交到事务Topic里面(两阶段提交的Prepared阶段)。不是事务消息,则将消息写入消息存储 MessageStore,这块逻辑我们前面已经分析过了,就是将消息追加到 CommitLog 中,然后分发到消费队列 ConsumeQueue 和索引 IndexFile 中。

java 复制代码
private CompletableFuture<RemotingCommand> asyncSendMessage(ChannelHandlerContext ctx, RemotingCommand request,
                                                            SendMessageContext mqtraceContext,
                                                            SendMessageRequestHeader requestHeader) {
    // 响应,消息预发送                                                         
    final RemotingCommand response = preSend(ctx, request, requestHeader);
    final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader)response.readCustomHeader();
    // 消息检查
    super.msgCheck(ctx, requestHeader, response);
    if (response.getCode() != -1) {
        return CompletableFuture.completedFuture(response);
    }
    
    // broker内部的消息结构
    MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
    msgInner.setTopic(requestHeader.getTopic());
    msgInner.setQueueId(requestHeader.getQueueId());
    msgInner.setBody(request.getBody());
    msgInner.setFlag(requestHeader.getFlag());
    Map<String, String> origProps = MessageDecoder.string2messageProperties(requestHeader.getProperties());
    MessageAccessor.setProperties(msgInner, origProps);
    ...

    CompletableFuture<PutMessageResult> putMessageResult = null;
    String transFlag = origProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
    // 事务消息
    if (transFlag != null && Boolean.parseBoolean(transFlag)) {
        // 事务消息准备
        putMessageResult = this.brokerController.getTransactionalMessageService().asyncPrepareMessage(msgInner);
    } else {
        // 基于消息存储区追加消息
        putMessageResult = this.brokerController.getMessageStore().asyncPutMessage(msgInner);
    }
    // 处理消息结果
    return handlePutMessageResultFuture(putMessageResult, response, request, msgInner, responseHeader, mqtraceContext, ctx, queueIdInt);
}

在消息检查中,主要对topic本身的写入权限、名称等做了校验。最重要的是,如果当前Broker中 TopicConfigManager 没有对应 topic 的配置元数据,则会自动创建一个根据客户端指定的默认topic配置创建一个 TopicConfig 元数据。

这块我们前面也分析过,TopicConfigManager 提供了一个方法,在发送消息时自动创建 TopicConfig,创建好之后,还会向所有 NameServer 重新注册,上报最新的topic信息。

java 复制代码
protected RemotingCommand msgCheck(ChannelHandlerContext ctx, SendMessageRequestHeader requestHeader, RemotingCommand response) {
    // 写入权限检查
    // topic 名称检查
    // 不允许客户端发送消息的topic检查
    
    // 获取 TopicConfig
    TopicConfig topicConfig = getTopicConfigManager().selectTopicConfig(requestHeader.getTopic());
    if (null == topicConfig) {
        int topicSysFlag = 0;
        // 在消息发送时,创建 Topic
        topicConfig = getTopicConfigManager().createTopicInSendMessageMethod(
            requestHeader.getTopic(), requestHeader.getDefaultTopic(),
            RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
            requestHeader.getDefaultTopicQueueNums(), topicSysFlag);
        ...
    }
    // queueId 合法性检查
    return response;
}

Topic 路由数据获取

通过前面的分析,我们大概就了解topic元数据是如何创建,以及客户端是如何找到要发送的broker,这节来看下一个新的topic是什么时候创建以及路由数据是如何来的。

DefaultMQProducerImpl 内部发送消息时,会根据 topic 查找路由发布信息 TopicPublishInfo。如果是一个全新的topic,那本地 topicPublishInfoTable 表里肯定是没有的,这时会从 NameServer 获取新 topic 的路由数据,因为可能其它客户端去创建了这个topic。如果TopicPublishInfo里没有路由信息(broker地址),说明这是一个新的topic,那路由数据从何而来呢?

java 复制代码
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
    // 一个新 topic 时没有路由信息
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
        // 放入一个空的
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        // 从 nameserver 拉取路由信息(可能其它客户端创建了topic)
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
    }
    // 有路由信息,表面已经创建ok
    if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
        return topicPublishInfo;
    } else {
        // 没有路由信息,说明还没创建这个topic,这时拉取默认topic的路由数据更新到当前topic
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
        return topicPublishInfo;
    }
}

从NameServer更新路由信息的方法有一个 isDefault 参数来控制是否获取默认topic的路由信息。每个Broker启动时,TopicConfigManager 都会创建一个 TBW102 的默认topic,Broker会将自己的topic配置、地址等信息注册到 NameServer,通过这个默认的 TBW102 就可以知道一个topic分布到哪些个broker上。

所以当生产者发送消息是一个新的topic时,它会获取 TBW102 的路由数据复制为自己的路由数据, 然后更新本地的broker地址表 brokerAddrTable,更新生产者表 producerTable 中的路由发布信息,更新消费者表 consumerTable 中的订阅信息等。

java 复制代码
public boolean updateTopicRouteInfoFromNameServer(
        final String topic, // topic 名称
        boolean isDefault, // 是否取默认topic路由数据
        DefaultMQProducer defaultMQProducer) {
    try {
        try {
            TopicRouteData topicRouteData;
            if (isDefault && defaultMQProducer != null) {
                // 从NS获取默认topic(TBW102)路由数据
                topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(), clientConfig.getMqClientApiTimeout());
                if (topicRouteData != null) {
                    // 设置读写队列数量
                    for (QueueData data : topicRouteData.getQueueDatas()) {
                        // TBW102 的读写队列数为 8,本地默认读写队列为 4
                        int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
                        data.setReadQueueNums(queueNums);
                        data.setWriteQueueNums(queueNums);
                    }
                }
            } else {
                // 直接获取topic的路由数据
                topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, clientConfig.getMqClientApiTimeout());
            }
            if (topicRouteData != null) {
                // TopicRouteData 路由表
                TopicRouteData old = this.topicRouteTable.get(topic);
                boolean changed = topicRouteDataIsChange(old, topicRouteData);
                if (changed) {
                    // 更新broker地址表
                    for (BrokerData bd : topicRouteData.getBrokerDatas()) {
                        this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
                    }
                    // 更新生产者表的topic发布信息
                    if (!producerTable.isEmpty()) {
                        // 路由数据转换为topic发布信息
                        TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
                        publishInfo.setHaveTopicRouterInfo(true);
                        Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
                        while (it.hasNext()) {
                            MQProducerInner impl = it.next().getValue();
                            // 更新topic发布信息
                            impl.updateTopicPublishInfo(topic, publishInfo);
                        }
                    }
                    // 更新消费者表订阅信息
                    if (!consumerTable.isEmpty()) {
                        // 路由数据转换为订阅信息
                        Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
                        Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
                        while (it.hasNext()) {
                            MQConsumerInner impl = it.next().getValue();
                            // 更新订阅信息
                            impl.updateTopicSubscribeInfo(topic, subscribeInfo);
                        }
                    }
                    // 放入路由表
                    this.topicRouteTable.put(topic, topicRouteData);
                    return true;
                }
            }
        } catch (MQClientException e) {...}

    return false;
}

生产者总结

1、生产者架构

生产者发送消息的组件是 DefaultMQProducer,它主要是定义消息的命名空间、生产者组、指定默认的topic等消息基本属性。而消息发送能力是代理给 DefaultMQProducerImpl 来完成。

DefaultMQProducerImpl 是消息生产者实现,会注册到 MQClientInstance 作为一个生产者组件。它主要是自动去创建消息的topic,对消息进行一些必要的校验和设置等,然后找到topic对应的路由信息。

MQClientInstance 才是消息客户端实例,它负责管理客户端中的生产者和消费者,负责更新集群中的broker地址信息和topic路由信息。维护了与Broker通信的客户端组件 MQClientAPIImpl,其基于 NettyRemotingClient 实现与broker的远程通信。

2、消息发送流程

消息发送流程其实最核心的逻辑便是获取topic的路由,找到broker地址后,将消息发送到broker就行了。

首先,每组broker启动时都会创建一个默认的topic,即 TBW102,然后每个broker都会注册到NameServer,那么NS中的路由信息管理器 RouteInfoManager 就知道了 TBW102 的路由信息,其实最主要的信息就是 TBW102 这个 topic 可以发送到哪些 broker 上。

客户端会每隔30秒,更新本地的topic路由信息,因为 broker 可能会下线,路由信息可能有变更。

发送消息时,如果本地路由表中没有对应topic的路由信息,那么就会从NS复制 TBW102 的路由信息,以此来获取 topic 的路由数据。新topic是在消息发送到broker后,broker检查到本地没有TopicConfig,才会去创建topic配置,并注册到NS去。

消息发送到broker后,就是将消息写入CommitLog,然后将消息分发到消费队列 COnsumeQueue 和构建索引 IndexFile。

相关推荐
金灰4 分钟前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5
菜鸟一皓4 分钟前
IDEA的lombok插件不生效了?!!
java·ide·intellij-idea
爱上语文7 分钟前
Java LeetCode每日一题
java·开发语言·leetcode
bug菌30 分钟前
Java GUI编程进阶:多线程与并发处理的实战指南
java·后端·java ee
程序猿小D43 分钟前
第二百六十九节 JPA教程 - JPA查询OrderBy两个属性示例
java·开发语言·数据库·windows·jpa
极客先躯2 小时前
高级java每日一道面试题-2024年10月3日-分布式篇-分布式系统中的容错策略都有哪些?
java·分布式·版本控制·共识算法·超时重试·心跳检测·容错策略
夜月行者2 小时前
如何使用ssm实现基于SSM的宠物服务平台的设计与实现+vue
java·后端·ssm
程序猿小D2 小时前
第二百六十七节 JPA教程 - JPA查询AND条件示例
java·开发语言·前端·数据库·windows·python·jpa
Yvemil72 小时前
RabbitMQ 入门到精通指南
开发语言·后端·ruby
sdg_advance2 小时前
Spring Cloud之OpenFeign的具体实践
后端·spring cloud·openfeign