从源码寻找RocketMQ消费者消费起始位置的问题

RocketMQ消费者中有一个consumeFromWhere属性,该属性从语义上来看就是说该消费者从队列的哪里开始消费,并且可以通过setConsumeFormWhere方法去进行设置,可设置的有三个值,分别是CONSUME_FROM_LAST_OFFSET,CONSUME_FROM_FIRST_OFFSET,CONSUME_FORM_TIMESTAMP,按照语义上来理解就是从队列的最后开始消费,从队列的初始位置开始消费,从消息指定的时间戳开始消费,但是事实上真的是这么简单吗,下面我们就来看一下这三个值是怎么实现的。

源码分析

首先一个消费者开始消费之前,先要分配到自己订阅的主题对应的mq,这个过程就需要对所有的mq去进行负载均衡了,具体负载均衡的过程不是这篇文章的重点,我们直接来到计算开始消费位点的地方

org.apache.rocketmq.client.impl.consumer.RebalanceImpl#updateProcessQueueTableInRebalance

List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
for (MessageQueue mq : mqSet) {
    // 条件成立: 说明这个mq是新分配的
    if (!this.processQueueTable.containsKey(mq)) {

        // 如果当前消费者实例是顺序消费,那么就会先对新分配到的mq进行broker端加锁,如果加锁不成功,直接跳过
        if (isOrder && !this.lock(mq)) {
            log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
            continue;
        }

        // 内存中可能有该队列的一些脏数据,所以要把这些脏数据移除
        this.removeDirtyOffset(mq);
        // 创建该新分配的mq对应的队列快照对象
        ProcessQueue pq = new ProcessQueue();
        // 根据用户设置的ConsumeFromWhere去获取新分配的mq下一次起始消费偏移量,ConsumeFromWhere根据setConsumeFromWhere()方法进行设置
        long nextOffset = this.computePullFromWhere(mq);
        // 如果起始消费偏移量 >= 0, 就创建拉取消息的任务
        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);
                pullRequest.setMessageQueue(mq);
                pullRequest.setProcessQueue(pq);
                pullRequestList.add(pullRequest);
                changed = true;
            }
        }
        // 如果起始消费偏移量 < 0,就什么都不做,只打印个日志
        else {
            log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
        }
    }
}
// 把新分配的队列快照分发给PullMessageService线程
this.dispatchPullRequest(pullRequestList);

上面的代码是updateProcessQueueTableInRebalance方法的一部分,updateProcessQueueTableInRebalance方法是从负载均衡中调用过来的,mqSet这个集合是负载均衡之后分配给当前消费者实例的最新mq集合,processQueueTable里面保存了当前消费者之前被分配过的mq以及对应的ProcessQueue。首先去遍历所有新分配过来的mq,每一个新分配的mq与processQueueTable中的mq进行对比,如果发现processQueueTable中不存在这个mq,则表示这个mq是新分配过来的,此时就需要去创建该mq对应的ProcessQueue,然后放到processQueueTable中,这里注意有一行代码long nextOffset = this.computePullFromWhere(mq);这行代码返回了一个nextOffset,消费者根据这个nextOffset去构建出一个拉取消息请求给拉取消息服务,拉取消息服务根据这个nextOffset的值去决定从队列中的那个位置开始拉取消息,所以这行代码就是去计算队列的消费位置的了,我们进去看一看

org.apache.rocketmq.client.impl.consumer.RebalancePushImpl#computePullFromWhere

该方法是一个抽象方法,如果我们使用的是DefaultMQPushConsumer这个消费者实例,那么底层使用的就是RebalancePushImpl去进行消费开始位点的计算

public long computePullFromWhere(MessageQueue mq) {
    long result = -1;
    final ConsumeFromWhere consumeFromWhere = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getConsumeFromWhere();
    final OffsetStore offsetStore = this.defaultMQPushConsumerImpl.getOffsetStore();
    switch (consumeFromWhere) {
        case CONSUME_FROM_LAST_OFFSET_AND_FROM_MIN_WHEN_BOOT_FIRST:
        case CONSUME_FROM_MIN_OFFSET:
        case CONSUME_FROM_MAX_OFFSET:
        case CONSUME_FROM_LAST_OFFSET: {
            // 从broker端获取到当前消费者组对这个mq的已消费进度
            long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE);
            // 条件成立,这里有两种场景:
            // 1.当lastOffset > 0时, 说明当前消费组已经有实例消费过这个mq,直接返回broker端记录的消费进度
            // 2.当lastOffset == 0时, 表示当前这个消费者组第一次消费这个mq,并且这个mq的consumequeue文件还未曾被删除过,以及此时该mq内的消息是“热数据”,此时返回lastOffset == 0
            if (lastOffset >= 0) {
                result = lastOffset;
            }
            // 条件成立:表示当前这个消费者组第一次消费这个mq,但是该mq的consumequeue文件被删除过,或者 此时该mq内的消息是“冷数据”
            else if (-1 == lastOffset) {
                // 重试主题
                if (mq.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    result = 0L;
                }
                // 非重试主题
                else {
                    try {
                        // 取这个mq的最大偏移位(consumequeue目录中的最大偏移位)
                        result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq);
                    } catch (MQClientException e) {
                        result = -1;
                    }
                }
            } else {
                result = -1;
            }
            break;
        }
        case CONSUME_FROM_FIRST_OFFSET: {
            long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE);
            if (lastOffset >= 0) {
                result = lastOffset;
            } else if (-1 == lastOffset) {
                result = 0L;
            } else {
                result = -1;
            }
            break;
        }
        case CONSUME_FROM_TIMESTAMP: {
            long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE);
            if (lastOffset >= 0) {
                result = lastOffset;
            } else if (-1 == lastOffset) {
                if (mq.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    try {
                        result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq);
                    } catch (MQClientException e) {
                        result = -1;
                    }
                } else {
                    try {
                        long timestamp = UtilAll.parseDate(this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getConsumeTimestamp(),
                            UtilAll.YYYYMMDDHHMMSS).getTime();
                        result = this.mQClientFactory.getMQAdminImpl().searchOffset(mq, timestamp);
                    } catch (MQClientException e) {
                        result = -1;
                    }
                }
            } else {
                result = -1;
            }
            break;
        }

        default:
            break;
    }

    return result;
}

参数就是待计算开始消费位点的mq,这里有三个分支,分别就是我们上面说的三个枚举不同的计算方式,当我们设置的开始消费位点方式是CONSUME_FROM_LAST_OFFSET的时候会来到第一个分支

case CONSUME_FROM_LAST_OFFSET: {
    // 从broker端获取到当前消费者组对这个mq的已消费进度
    long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE);
    // 条件成立,这里有两种场景:
    // 1.当lastOffset > 0时, 说明当前消费组已经有实例消费过这个mq,直接返回broker端记录的消费进度
    // 2.当lastOffset == 0时, 表示当前这个消费者组第一次消费这个mq,并且这个mq的consumequeue文件还未曾被删除过,以及此时该mq内的消息是“热数据”,此时返回lastOffset == 0
    if (lastOffset >= 0) {
        result = lastOffset;
    }
    // 条件成立:表示当前这个消费者组第一次消费这个mq,但是该mq的consumequeue文件被删除过,或者 此时该mq内的消息是“冷数据”
    else if (-1 == lastOffset) {
        // 重试主题
        if (mq.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
            result = 0L;
        }
        // 非重试主题
        else {
            try {
                // 取这个mq的最大偏移位(consumequeue目录中的最大偏移位)
                result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq);
            } catch (MQClientException e) {
                result = -1;
            }
        }
    } else {
        result = -1;
    }
    break;
}

首先会通过消费进度组件(集群模式是RemoteBrokerOffsetStore)去从broker端获取这个mq的已消费进度

org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore#readOffset

/**
 * 获取mq的已消费偏移量
 * @param mq    mq对象
 * @param type  获取类型,内存获取 or broker端获取
 * @return
 */
@Override
public long readOffset(final MessageQueue mq, final ReadOffsetType type) {
    if (mq != null) {
        switch (type) {
            case MEMORY_FIRST_THEN_STORE:

                // 从内存中拿mq已消费偏移量,
            case READ_FROM_MEMORY: {
                AtomicLong offset = this.offsetTable.get(mq);
                if (offset != null) {
                    return offset.get();
                } else if (ReadOffsetType.READ_FROM_MEMORY == type) {
                    return -1;
                }
            }

                // 从远程broker端拿mq已消费偏移量
            case READ_FROM_STORE: {
                try {
                    // 从broker中查询指定mq已消费偏移量
                    long brokerOffset = this.fetchConsumeOffsetFromBroker(mq);
                    AtomicLong offset = new AtomicLong(brokerOffset);
                    // 更新offsetTable表
                    this.updateOffset(mq, offset.get(), false);
                    return brokerOffset;
                }
                // 查询失败fetchConsumeOffsetFromBroker方法会抛出异常,直接返回 -1
                catch (MQBrokerException e) {
                    return -1;
                }
                //Other exceptions
                catch (Exception e) {
                    log.warn("fetchConsumeOffsetFromBroker exception, " + mq, e);
                    return -2;
                }
            }
            default:
                break;
        }
    }

    return -1;
}

org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore#fetchConsumeOffsetFromBroker

**
     * 从broker中获取到指定mq已消费偏移量
     * @param mq    指定的mq
     * @return  指定的mq的已消费偏移量
     * @throws RemotingException
     * @throws MQBrokerException
     * @throws InterruptedException
     * @throws MQClientException
     */
    private long fetchConsumeOffsetFromBroker(MessageQueue mq) throws RemotingException, MQBrokerException,
        InterruptedException, MQClientException {
        // 根据mq所属的broker组名找到其中的主节点或者从节点地址
        FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInAdmin(mq.getBrokerName());
        if (null == findBrokerResult) {

            this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic());
            findBrokerResult = this.mQClientFactory.findBrokerAddressInAdmin(mq.getBrokerName());
        }

        // 往broker发送查询该队列的消费偏移量的请求
        if (findBrokerResult != null) {
            // 创建请求头对象
            QueryConsumerOffsetRequestHeader requestHeader = new QueryConsumerOffsetRequestHeader();
            // mq所属主题
            requestHeader.setTopic(mq.getTopic());
            // 消费者组名
            requestHeader.setConsumerGroup(this.groupName);
            // queueId
            requestHeader.setQueueId(mq.getQueueId());

            // 查询失败抛出异常
            return this.mQClientFactory.getMQClientAPIImpl().queryConsumerOffset(
                findBrokerResult.getBrokerAddr(), requestHeader, 1000 * 5);
        } else {
            throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);
        }
    }
}

org.apache.rocketmq.client.impl.MQClientAPIImpl#queryConsumerOffset

/**
 * 查询指定队列的消费偏移量
 * @param addr  broker地址
 * @param requestHeader 请求头对象
 * @param timeoutMillis 请求超时时间
 * @return  队列已消费偏移量
 * @throws RemotingException
 * @throws MQBrokerException
 * @throws InterruptedException
 */
public long queryConsumerOffset(
    final String addr,
    final QueryConsumerOffsetRequestHeader requestHeader,
    final long timeoutMillis
) throws RemotingException, MQBrokerException, InterruptedException {
    RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_CONSUMER_OFFSET, requestHeader);

    RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr),
        request, timeoutMillis);
    assert response != null;
    switch (response.getCode()) {
            // 查询成功
        case ResponseCode.SUCCESS: {
            QueryConsumerOffsetResponseHeader responseHeader =
                (QueryConsumerOffsetResponseHeader) response.decodeCommandCustomHeader(QueryConsumerOffsetResponseHeader.class);

            return responseHeader.getOffset();
        }
        default:
            break;
    }

    // 返回QUERY_NOT_FOUND响应码,表示查询失败,抛出异常
    throw new MQBrokerException(response.getCode(), response.getRemark(), addr);
}

可以看到上面就是向broker发起查询指定mq的已消费进度的请求,所以我们下面要去到broker服务端看是怎么处理返回的

org.apache.rocketmq.broker.processor.ConsumerManageProcessor#queryConsumerOffset

private RemotingCommand queryConsumerOffset(ChannelHandlerContext ctx, RemotingCommand request)
    throws RemotingCommandException {
    final RemotingCommand response =
        RemotingCommand.createResponseCommand(QueryConsumerOffsetResponseHeader.class);
    final QueryConsumerOffsetResponseHeader responseHeader =
        (QueryConsumerOffsetResponseHeader) response.readCustomHeader();
    final QueryConsumerOffsetRequestHeader requestHeader =
        (QueryConsumerOffsetRequestHeader) request
            .decodeCommandCustomHeader(QueryConsumerOffsetRequestHeader.class);

    // 从broker内存中查询到指定消费者组 在指定主题和指定mq的 已消费偏移量, 如果这个消费者组是第一次消费该mq,那么就查询不到对应的消费进度了,此时返回-1
    long offset =
        this.brokerController.getConsumerOffsetManager().queryOffset(
            requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId());

    // 查询成功,直接返回给客户端
    if (offset >= 0) {
        responseHeader.setOffset(offset);
        response.setCode(ResponseCode.SUCCESS);
        response.setRemark(null);
    }
    // 如果查询不到,上面的offset则会返回 -1, 那么会进此条件分支
    else {
        // 获取mq中最小的偏移位
        long minOffset =
            this.brokerController.getMessageStore().getMinOffsetInQueue(requestHeader.getTopic(),
                requestHeader.getQueueId());
        // 条件成立: minOffset == 0(说明该mq的consumequeue文件还未曾被删除过),并且此时该mq的消息并不是“冷数据”,那么这种情况返回的offset就等于0
        if (minOffset <= 0
            && !this.brokerController.getMessageStore().checkInDiskByConsumeOffset(
            requestHeader.getTopic(), requestHeader.getQueueId(), 0)) {
            responseHeader.setOffset(0L);
            response.setCode(ResponseCode.SUCCESS);
            response.setRemark(null);
        }
        // 上面条件不成立,返回查询未找到
        else {
            response.setCode(ResponseCode.QUERY_NOT_FOUND);
            response.setRemark("Not found, V3_0_6_SNAPSHOT maybe this group consumer boot first");
        }
    }

    return response;
}

在broker中首先会从内存中查询出指定消费者组在指定主题和指定mq的已消费偏移量

/**
 * 查询指定消费者组,指定topic,指定mq的已消费进度,如果查询不到,返回-1
 * @param group 指定的消费者组名
 * @param topic 指定的主题名称
 * @param queueId   指定的mq的queueId
 * @return  对应的已消费进度
 */
public long queryOffset(final String group, final String topic, final int queueId) {
    // topic@group
    String key = topic + TOPIC_GROUP_SEPARATOR + group;
    ConcurrentMap<Integer, Long> map = this.offsetTable.get(key);
    if (null != map) {
        Long offset = map.get(queueId);
        if (offset != null)
            return offset;
    }

    return -1;
}

根据topic和消费者组名作为key去获取指定mq的已消费进度,也就是说同一个topic对于不同的消费者组来说,他们对于同一个mq的消费进度是不同的。最后如果没有查询到就返回-1,那么什么时候会没有查询到?就是这个消费组第一次消费这个topic中的这个mq。接下来就会去判断上面查询到的offset

if (offset >= 0) {
    responseHeader.setOffset(offset);
    response.setCode(ResponseCode.SUCCESS);
    response.setRemark(null);
}

如果查询到的话这个if条件就会成立,那么此时就立刻返回给消费者,但是如果查询不到就会进入else分支

else {
    long minOffset =
        this.brokerController.getMessageStore().getMinOffsetInQueue(requestHeader.getTopic(),
            requestHeader.getQueueId());
    if (minOffset <= 0
        && !this.brokerController.getMessageStore().checkInDiskByConsumeOffset(
        requestHeader.getTopic(), requestHeader.getQueueId(), 0)) {
        responseHeader.setOffset(0L);
        response.setCode(ResponseCode.SUCCESS);
        response.setRemark(null);
    }
    // 上面条件不成立,返回查询未找到
    else {
        response.setCode(ResponseCode.QUERY_NOT_FOUND);
        response.setRemark("Not found, V3_0_6_SNAPSHOT maybe this group consumer boot first");
    }
}

在else分支中会去查询到这个mq此时的最小偏移位,并且对返回的这个mq的最小偏移位进行判断,这里关键要搞清楚这个条件是如何成立的。第一个判断minOffset<=0,那么minOffset是什么时候等于0的呢?其实就是这个mq的consumequeue文件没有被删除过(定时任务删除或者手动删除),第二个判断是判断此时consumequeue文件中的数据是否是“冷数据”,如果不是“冷数据”这个判断就成立,所以综合来说整个if判断成立的条件是:该mq的consumequeue文件还未曾被删除过,并且此时该mq的消息并不是“冷数据”,那么这个条件符合之后,返回给消费者的offset就是0,反之如果if条件不成立,就返回给消费者一个QUERY_NOT_FOUND查询未找到的响应码

分析完broker服务端之后,我们再回到消费者端看拿到返回值之后如何处理

public long queryConsumerOffset(
    final String addr,
    final QueryConsumerOffsetRequestHeader requestHeader,
    final long timeoutMillis
) throws RemotingException, MQBrokerException, InterruptedException {
    RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_CONSUMER_OFFSET, requestHeader);

    RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr),
        request, timeoutMillis);
    assert response != null;
    switch (response.getCode()) {
            // 查询成功
        case ResponseCode.SUCCESS: {
            QueryConsumerOffsetResponseHeader responseHeader =
                (QueryConsumerOffsetResponseHeader) response.decodeCommandCustomHeader(QueryConsumerOffsetResponseHeader.class);

            return responseHeader.getOffset();
        }
        default:
            break;
    }

    // 返回QUERY_NOT_FOUND响应码,表示查询失败,抛出异常
    throw new MQBrokerException(response.getCode(), response.getRemark(), addr);
}

可以看到对于返回SUCCESS响应码的情况,直接就拿到broker返回的offset并返回,而对于其他响应码,也就是QUERY_NOT_FOUND这个响应码都会抛出异常,然后我们再看上层调用

case READ_FROM_STORE: {
    try {
        // 从broker中查询指定mq已消费偏移量
        long brokerOffset = this.fetchConsumeOffsetFromBroker(mq);
        AtomicLong offset = new AtomicLong(brokerOffset);
        // 更新offsetTable表
        this.updateOffset(mq, offset.get(), false);
        return brokerOffset;
    }
    // 查询失败fetchConsumeOffsetFromBroker方法会抛出异常,直接返回 -1
    catch (MQBrokerException e) {
        return -1;
    }
    //Other exceptions
    catch (Exception e) {
        log.warn("fetchConsumeOffsetFromBroker exception, " + mq, e);
        return -2;
    }
}

一直到这个上层调用的try catch代码块,如果查询成功就直接返回出去了,否则对于查询失败的情况由于底层会抛出异常,所以这里的catch语句块就能够执行,返回-1。分析到这里,我们再回到computePullFromWhere方法:

case CONSUME_FROM_LAST_OFFSET: {
    long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE);
        result = lastOffset;
    }
    if (lastOffset >= 0) {
        result = lastOffset;
    }
    else if (-1 == lastOffset) {
        // 重试队列
        if (mq.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
            result = 0L;
        }
        // 非重试队列
        else {
            try {
                result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq);
            } catch (MQClientException e) {
                result = -1;
            }
        }
    } else {
        result = -1;
    }
    break;
}

上面说到从broker服务端获取到mq已消费的偏移量的细节,那么这里就要对获取到的已消费偏移量去进行判断,从而得到正确的值返回给上层,这里先说明一下,在上层的调用中如果该方法返回的mq已消费偏移量是小于0的,那么是不会对该mq进行拉取消息的。

根据上面的分析,可以知道下面的三个if条件分支分别是什么情况下会发生:

1.lastOffset>=0,什么情况下返回的mq已消费偏移量才会大于等于0?这里就分为大于0和等于0这两种情况了:

        当lastOffset > 0时, 表示当前消费组已经有实例消费过这个mq,直接返回broker端记录的消费进度

        当lastOffset == 0时, 表示当前这个消费者组第一次消费这个mq,并且这个mq的consumequeue文件还未曾被删除过,以及此时该mq内的消息是“热数据”

2.lastOffset==-1

        当这个消费者组第一次消费这个mq,但是该mq的consumequeue文件被删除过,或者 此时该mq内的消息是“冷数据”的时候

3.else other

        其他情况都返回-1

所以此时这里就会有两个问题了:

1.当启动一个新的消费者组实例去消费消息,此时该消费者实例对于所分配到的mq是从哪里开始消费?

根据上面的分析,我们可以知道,此时是有可能会进入第一个if分支或者是第二个if分支,关键就在于此时该mq的consumequeue文件是否有被删除过,或者是该mq内的数据是“冷数据”还是“热数据”,如果该消费者实例启动得比较晚,此时定时任务已经把过期的consumequeue文件都删除了,那么就从该mq的最新消息开始消费了,又或者是此时该mq中的消息堆积得比较多了,已经超过了总体内存的30%(冷数据),那么也会从该mq的最新消息开始消费,其余情况都会重新消费该mq的消息

2.此时已经有一个消费者组正在消费,新增一个该消费者组的实例,那么此时该消费者实例对于所分配到的mq是从哪里开始消费?

这种情况下,第一个if条件就会成立,所以是从broker返回的该mq已消费偏移量开始消费

我们有了上面分析,接着再去分析其他两个值

  • CONSUME_FROM_FIRST_OFFSET
case CONSUME_FROM_FIRST_OFFSET: {
    long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE);
    if (lastOffset >= 0) {
        result = lastOffset;
    } else if (-1 == lastOffset) {
        result = 0L;
    } else {
        result = -1;
    }
    break;
}

可以看到CONSUME_FROM_FIRST_OFFSET的判断与CONSUME_FROM_LAST_OFFSET的判断大同小异,唯一不同的就是对于lastOffset等于-1的这种情况,这种情况统一都是返回0,也就是说当这个消费者组第一次消费这个mq,但是该mq的consumequeue文件被删除过,或者 此时该mq内的消息是“冷数据”的时候就会重新消费该mq的消息,所以可以看到,对于设置了CONSUME_FROM_FIRST_OFFSET的消费者,如果当前是已经有对应的消费组的实例在消费了,那么此时该值并不会生效

  • CONSUME_FROM_TIMESTAMP
case CONSUME_FROM_TIMESTAMP: {
    long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE);
    if (lastOffset >= 0) {
        result = lastOffset;
    } else if (-1 == lastOffset) {
        if (mq.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
            try {
                result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq);
            } catch (MQClientException e) {
                result = -1;
            }
        } else {
            try {
                long timestamp = UtilAll.parseDate(this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getConsumeTimestamp(),
                    UtilAll.YYYYMMDDHHMMSS).getTime();
                result = this.mQClientFactory.getMQAdminImpl().searchOffset(mq, timestamp);
            } catch (MQClientException e) {
                result = -1;
            }
        }
    } else {
        result = -1;
    }
    break;
}

CONSUME_FROM_TIMESTAMP中对于lastOffset>=0的处理是一样的,不同的就是last等于-1的时候,是会根据getConsumeTimestamp指定的时间去找到这个时间的消息偏移量并返回出去,也就是说对于设置了CONSUME_FROM_TIMESTAMP的消费者,如果当前是已经有对应的消费组的实例在消费了,那么此时该值也是不会生效

总结

我们可以看到,根据上面的分析,消费者设置这三个值所起到的效果并不会跟其语义是完全一致的,如果启动的消费者所属的消费者组中已经有实例对mq进行消费了,那么这个消费者消费的起始位置就是已消费偏移量,反之,如果这个消费者所属的消费者组是第一次对mq进行消费,就可能会根据自己的语义来决定了

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个基于Java中的线程和锁机制实现的生产者消费者问题的代码,使用了阻塞队列来实现: ```java import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class ProducerConsumer { public static void main(String[] args) { BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); Thread producer = new Thread(new Producer(queue)); Thread consumer = new Thread(new Consumer(queue)); producer.start(); consumer.start(); } } class Producer implements Runnable { private final BlockingQueue<Integer> queue; public Producer(BlockingQueue<Integer> queue) { this.queue = queue; } @Override public void run() { for (int i = 0; i < 10; i++) { try { queue.put(i); System.out.println("Produced: " + i); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Consumer implements Runnable { private final BlockingQueue<Integer> queue; public Consumer(BlockingQueue<Integer> queue) { this.queue = queue; } @Override public void run() { while (true) { try { Integer value = queue.take(); System.out.println("Consumed: " + value); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } ``` 在此示例中,我们创建了一个BlockingQueue来存储生产者生成的整数。生产者线程使用put()方法将整数添加到队列中,而消费者线程则使用take()方法从队列中移除整数。阻塞队列会自动处理同步和线程安全问题。 在这个示例中,生产者线程将10个整数添加到队列中,消费者线程将无限循环地从队列中读取整数。在输出中,您将看到生产者和消费者线程交替执行,并且由于我们设置了不同的睡眠时间,消费者线程比生产者线程执行得更慢。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值