✨Kafka✨生产者消息发送源码解析✨

431 阅读14分钟

你的名字-001

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈


前言

本文将对Kafka生产者消息发送的源码进行分析,帮助了解Kafka生产者发送消息时,回调函数如何执行Kafka消息发送的异步和同步原理以及Kafka消息发送的机制

Kafka版本:3.1.1

正文

一. 认识生产者消息ProducerRecord

Kafka客户端的生产者对应的类是KafkaProducer,其有两个发送方法,签名如下。

// 没有回调函数的消息发送
public Future<RecordMetadata> send(ProducerRecord<K, V> record)
// 有回调函数的消息发送
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback)

回调函数我们后面再讨论,但是可以先了解一下生产者消息ProducerRecord,其有如下字段。

public class ProducerRecord<K, V> {

    // 消息主题
    private final String topic;
    // 分区
    private final Integer partition;
    // 消息头
    private final Headers headers;
    // 消息键
    private final K key;
    // 消息值
    private final V value;
    // 消息时间戳
    private final Long timestamp;

    ......

}

如果通过KafkaProducer来发送消息,我们可以先将ProducerRecord创建出来,然后通过KafkaProducersend() 方法完成发送,而在创建ProducerRecord时,我们可以通过构造函数指定消息主题分区消息头消息键消息值消息时间戳

二. 应用生产者拦截器

可以为Kafka生产者添加拦截器ProducerInterceptor,其作用时机如下图所示。

Kafka-生产者拦截器作用示意图

三. 拉取集群元数据信息

这是Kafka生产者发送消息时唯一会阻塞的地方。

KafkaProducerdoSend() 方法中,会调用到waitOnMetadata() 方法,该方法实现如下。

private ClusterAndWaitTime waitOnMetadata(String topic, Integer partition, long nowMs, long maxWaitMs) throws InterruptedException {
    Cluster cluster = metadata.fetch();

    if (cluster.invalidTopics().contains(topic))
        throw new InvalidTopicException(topic);

    metadata.add(topic, nowMs);

    Integer partitionsCount = cluster.partitionCountForTopic(topic);
    if (partitionsCount != null && (partition == null || partition < partitionsCount))
        return new ClusterAndWaitTime(cluster, 0);

    long remainingWaitMs = maxWaitMs;
    long elapsed = 0;
    do {
        if (partition != null) {
            log.trace("Requesting metadata update for partition {} of topic {}.", partition, topic);
        } else {
            log.trace("Requesting metadata update for topic {}.", topic);
        }
        metadata.add(topic, nowMs + elapsed);
        int version = metadata.requestUpdateForTopic(topic);
        // 唤醒Sender去拉取元数据
        sender.wakeup();
        try {
            // 阻塞在这里等待元数据拉取完成
            metadata.awaitUpdate(version, remainingWaitMs);
        } catch (TimeoutException ex) {
            // 阻塞超时则返回TimeoutException
            throw new TimeoutException(
                    String.format("Topic %s not present in metadata after %d ms.",
                            topic, maxWaitMs));
        }
        cluster = metadata.fetch();
        elapsed = time.milliseconds() - nowMs;
        if (elapsed >= maxWaitMs) {
            throw new TimeoutException(partitionsCount == null ?
                    String.format("Topic %s not present in metadata after %d ms.",
                            topic, maxWaitMs) :
                    String.format("Partition %d of topic %s with partition count %d is not present in metadata after %d ms.",
                            partition, topic, partitionsCount, maxWaitMs));
        }
        metadata.maybeThrowExceptionForTopic(topic);
        remainingWaitMs = maxWaitMs - elapsed;
        partitionsCount = cluster.partitionCountForTopic(topic);
    } while (partitionsCount == null || (partition != null && partition >= partitionsCount));

    return new ClusterAndWaitTime(cluster, elapsed);
}

四. 序列化消息的键和值

消息的keyvalue在发送前需要序列化,对应代码片段在KafkaProducerdoSend() 方法中,代码片段如下。

byte[] serializedKey;
try {
    serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
} catch (ClassCastException cce) {
    throw new SerializationException("Can't convert key of class " + record.key().getClass().getName() +
            " to class " + producerConfig.getClass(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG).getName() +
            " specified in key.serializer", cce);
}
byte[] serializedValue;
try {
    serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
} catch (ClassCastException cce) {
    throw new SerializationException("Can't convert value of class " + record.value().getClass().getName() +
            " to class " + producerConfig.getClass(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG).getName() +
            " specified in value.serializer", cce);
}

五. 计算消息所属分区

使用自定义或默认的分区器获取消息所属分区,对应代码片段在KafkaProducerdoSend() 方法中,代码片段如下。

int partition = partition(record, serializedKey, serializedValue, cluster);

partition() 方法如下所示。

private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
    Integer partition = record.partition();
    return partition != null ?
            partition :
            partitioner.partition(
                    record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
}

分区计算策略总结如下。

  1. 消息中指定了分区。此时使用指定的分区;
  2. 消息中未指定分区但有自定义分区器。此时使用自定义分区器计算分区;
  3. 消息中未指定分区也没有自定义分区器但消息键不为空。此时对键求哈希值,并用求得的哈希值对Topic的分区数取模得到分区;
  4. 如果前面都不满足。此时根据Topic取一个递增整数并对Topic分区数求模得到分区。

六. 将消息添加到消息累加器

消息累加器RecordAccumulator,在RecordAccumulator中有一个字段叫做batches,其签名如下。

ConcurrentMap<TopicPartition, Deque<ProducerBatch>> batches

也就是根据Topic和分区可以唯一确定一个ProducerBatch的双端队列Deque,新添加的消息会被添加到队列最后一个节点上的ProducerBatch中,具体就是将消息写入到ProducerBatchMemoryRecordsBuilder中,而如果ProducerBatch写满了,则创建新的ProducerBatch然后往里面写。

消息添加到消息累加器后,,会得到一个RecordAppendResult,该对象包含如下信息。

public final static class RecordAppendResult {
    public final FutureRecordMetadata future;
    public final boolean batchIsFull;
    public final boolean newBatchCreated;
    public final boolean abortForNewBatch;

    public RecordAppendResult(FutureRecordMetadata future, boolean batchIsFull, 
                              boolean newBatchCreated, boolean abortForNewBatch) {
        this.future = future;
        this.batchIsFull = batchIsFull;
        this.newBatchCreated = newBatchCreated;
        this.abortForNewBatch = abortForNewBatch;
    }
}

关键的是RecordAppendResult中包含的FutureRecordMetadata,这就是发送的一个结果future,通过这个结果future可以拿到发送后的消息的元数据RecordMetadata,或者得到消息发送的异常,如果业务代码中是通过KafkaProducersend() 方法来发送消息,那么业务代码调用send() 方法持有的结果就是这个future

七. 唤醒Sender

当将消息添加到消息累加器后,我们会拿到RecordAppendResult,该对象的batchIsFull表示ProducerBatch满了)或newBatchCreated表示新创建了ProducerBatch)为true时,就会唤醒Sender来发送消息,对应代码片段在KafkaProducerdoSend() 方法中,如下所示。

if (result.batchIsFull || result.newBatchCreated) {
    log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
    this.sender.wakeup();
}

Sender是一个Runnable,所以直接看Senderrun() 方法,如下所示。

@Override
public void run() {
    log.debug("Starting Kafka producer I/O thread.");

    // 主要在这里进行循环
    while (running) {
        try {
            runOnce();
        } catch (Exception e) {
            log.error("Uncaught error in kafka producer I/O thread: ", e);
        }
    }

    ......

}

runOnce() 方法会调用到sendProducerData() 方法,在该方法中会从RecordAccumulator里将所有可发送的ProducerBatch获取出来并按照Broker进行分组,然后调用sendProduceRequests() 方法进行发送,sendProduceRequests() 方法实现如下。

private void sendProduceRequests(Map<Integer, List<ProducerBatch>> collated, long now) {
    // 按Broker对应的ProducerBatch集合来发送
    for (Map.Entry<Integer, List<ProducerBatch>> entry : collated.entrySet())
        sendProduceRequest(now, entry.getKey(), acks, requestTimeoutMs, entry.getValue());
}

实际就是将一个Broker的所有可发送的ProducerBatch合并到一个Request中进行发送,继续跟进一下sendProduceRequest() 方法,如下所示。

private void sendProduceRequest(long now, int destination, short acks, int timeout, List<ProducerBatch> batches) {
    
    ......
    
    ClientRequest clientRequest = client.newClientRequest(nodeId, requestBuilder, now, acks != 0, requestTimeoutMs, callback);
    // 这里使用NetworkClient来发送Request到对应Broker
    client.send(clientRequest, now);
    log.trace("Sent produce request to {}: {}", nodeId, requestBuilder);
}

使用NetworkClient发送RequestBroker时,最终会调用到NetworkClientdoSend() 方法,如下所示。

private void doSend(ClientRequest clientRequest, boolean isInternalRequest, long now, AbstractRequest request) {
    String destination = clientRequest.destination();
    RequestHeader header = clientRequest.makeHeader(request.version());
    if (log.isDebugEnabled()) {
        log.debug("Sending {} request with header {} and timeout {} to node {}: {}",
                  clientRequest.apiKey(), header, clientRequest.requestTimeoutMs(), destination, request);
    }
    Send send = request.toSend(header);
    // 将Request封装成InFlightRequest
    InFlightRequest inFlightRequest = new InFlightRequest(
        clientRequest,
        header,
        isInternalRequest,
        request,
        send,
        now);
    // 将Request对应的InFlightRequest添加到inFlightRequests缓冲区
    // InFlightRequest表示已发送且待确认的请求
    this.inFlightRequests.add(inFlightRequest);
    selector.send(new NetworkSend(clientRequest.destination(), send));
}

请求Request会被封装成InFlightRequest然后添加到inFlightRequests缓冲区,InFlightRequest表示已发送且待确认的请求,当RequestSelector完成发送并收到服务端的ACK后,客户端这边就会将Request对应的InFlightRequestinFlightRequests缓冲区移除。

八. 消息发送后的回调

我们通过KafkaProducer的如下send() 方法发送消息时,可以传入回调函数。

@Override
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
    ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
    return doSend(interceptedRecord, callback);
}

KafkaProducerdoSend() 方法中将消息添加到RecordAccumulator时会一并添加回调函数,代码片段如下。

// callback就是调用KafkaProducer的doSend()方法时传入的回调函数
Callback interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp);

// 将消息添加到RecordAccumulator时会一并将回调函数也添加进去
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
        serializedValue, headers, interceptCallback, remainingWaitMs, true, nowMs);

而将消息添加到RecordAccumulator时其实就是将消息写入到一个ProducerBatch中,此时interceptCallback也会一并添加,如下所示。

private RecordAppendResult tryAppend(long timestamp, byte[] key, byte[] value, Header[] headers,
                                    Callback callback, Deque<ProducerBatch> deque, long nowMs) {
    // 拿到待写入的ProducerBatch
    ProducerBatch last = deque.peekLast();
    if (last != null) {
        // 写入消息时也会一并添加回调函数
        FutureRecordMetadata future = last.tryAppend(timestamp, key, value, headers, callback, nowMs);
        if (future == null)
            last.closeForRecordAppends();
        else
            return new RecordAppendResult(future, deque.size() > 1 || last.isFull(), false, false);
    }
    return null;
}

所以继续跟进ProducerBatchtryAppend() 方法,看一下做了什么事情。

public FutureRecordMetadata tryAppend(long timestamp, byte[] key, byte[] value, Header[] headers, Callback callback, long now) {
    if (!recordsBuilder.hasRoomFor(timestamp, key, value, headers)) {
        return null;
    } else {
        this.recordsBuilder.append(timestamp, key, value, headers);
        this.maxRecordSize = Math.max(this.maxRecordSize, AbstractRecords.estimateSizeInBytesUpperBound(magic(),
                recordsBuilder.compressionType(), key, value, headers));
        this.lastAppendTime = now;
        // 在这里创建FutureRecordMetadata
        FutureRecordMetadata future = new FutureRecordMetadata(this.produceFuture, this.recordCount,
                                                                timestamp,
                                                                key == null ? -1 : key.length,
                                                                value == null ? -1 : value.length,
                                                                Time.SYSTEM);
        // 基于回调函数和FutureRecordMetadata创建一个Thunk
        // 然后将创建出来的Thunk添加到当前ProducerBatch的thunks集合中
        thunks.add(new Thunk(callback, future));
        this.recordCount++;
        return future;
    }
}

ProducerBatchtryAppend() 方法中,做了如下两件我们关心的事情。

  1. 创建了FutureRecordMetadata。通过该future可以拿到消息发送后的元数据信息RecordMetadata或发送时的异常信息;
  2. 创建了Thunk并添加到ProducerBatchthunks集合。将回调函数和FutureRecordMetadata创建了一个Thunk并添加到当前ProducerBatchthunks集合中。

Thunk其实就是将回调函数和FutureRecordMetadata做了一下关联,那么肯定在某一刻,会使用到Thunk,然后通过FutureRecordMetadata拿到消息发送元数据metadata或者异常exception,再然后就将metadataexception传入回调函数。

所以现在问题转移到了Thunk在哪里被使用的。回顾一下在Sender中,其sendProduceRequest() 方法有如下代码片段。

// RequestCompletionHandler顾名思义就是请求完毕后的处理器
RequestCompletionHandler callback = response -> handleProduceResponse(response, recordsByPartition, time.milliseconds());

String nodeId = Integer.toString(destination);
ClientRequest clientRequest = client.newClientRequest(nodeId, requestBuilder, now, acks != 0,
        requestTimeoutMs, callback);
client.send(clientRequest, now);

我们在发送ProducerBatch时,会将同一个Broker上的可以发送的ProducerBatch合并到一个Request中进行发送,这里的Request就是ClientRequest,在创建ClientRequest时会传入一个回调函数,这个回调函数在请求结束时会被调用,也就是请求结束时会调用到SenderhandleProduceResponse() 方法,此时当前请求对应的所有ProducerBatch已经被发送,因此就要调用SendercompleteBatch() 方法来将发送的每个ProducerBatch关闭,SendercompleteBatch() 方法实现如下。

private void completeBatch(ProducerBatch batch, ProduceResponse.PartitionResponse response, long correlationId, long now) {
    Errors error = response.error;

    if (error == Errors.MESSAGE_TOO_LARGE && batch.recordCount > 1 && !batch.isDone() &&
        (batch.magic() >= RecordBatch.MAGIC_VALUE_V2 || batch.isCompressed())) {
        // 如果当前ProducerBatch太长则分割后重新发送
        log.warn(
            "Got error produce response in correlation id {} on topic-partition {}, splitting and retrying ({} attempts left). Error: {}",
            correlationId,
            batch.topicPartition,
            this.retries - batch.attempts(),
            formatErrMsg(response));
        if (transactionManager != null)
            transactionManager.removeInFlightBatch(batch);
        this.accumulator.splitAndReenqueue(batch);
        maybeRemoveAndDeallocateBatch(batch);
        this.sensors.recordBatchSplit();
    } else if (error != Errors.NONE) {
        // 如果发生了异常
        if (canRetry(batch, response, now)) {
            // 若ProducerBatch没有超时
            // 且没有达到最大重试次数
            // 且ProducerBatch没有进入终态即成功或异常
            // 且当前发生的异常是可以重试的异常
            // 此时ProducerBatch需要重新发送
            log.warn("Got error produce response with correlation id {} on topic-partition {}, retrying ({} attempts left). Error: {}",
                correlationId,
                batch.topicPartition,
                this.retries - batch.attempts() - 1,
                formatErrMsg(response));
            reenqueueBatch(batch, now);
        } else if (error == Errors.DUPLICATE_SEQUENCE_NUMBER) {
            // 如果是重复序列号异常
            // 此时返回消息发送成功
            // 但是不会返回消息偏移和消息时间戳
            completeBatch(batch, response);
        } else {
            // 以消息发送失败的方式来关闭ProducerBatch
            failBatch(batch, response, batch.attempts() < this.retries);
        }
        if (error.exception() instanceof InvalidMetadataException) {
            if (error.exception() instanceof UnknownTopicOrPartitionException) {
                log.warn("Received unknown topic or partition error in produce request on partition {}. The " +
                         "topic-partition may not exist or the user may not have Describe access to it",
                         batch.topicPartition);
            } else {
                log.warn("Received invalid metadata error in produce request on partition {} due to {}. Going " +
                         "to request metadata update now", batch.topicPartition,
                         error.exception(response.errorMessage).toString());
            }
            metadata.requestUpdate();
        }
    } else {
        // 以消息发送成功的方式来关闭ProducerBatch
        completeBatch(batch, response);
    }

    if (guaranteeMessageOrder)
        this.accumulator.unmutePartition(batch.topicPartition);
}

上述方法做的事情概括如下。

  1. 如果ProducerBatch发送失败且允许重试则重新发送ProducerBatch。允许重试的条件有:ProducerBatch没有超时没有达到最大重试次数ProducerBatch没有进入终态当前发生的异常是可以重试的异常
  2. 如果ProducerBatch发送失败且不允许重试则执行failBatch()。此时就是以消息发送失败的方式来关闭ProducerBatch
  3. 如果ProducerBatch发送成功则执行completeBatch()。此时就是以消息发送成功的方式来关闭ProducerBatch

因此关闭ProducerBatch要么调用到failBatch() 方法,要么调用到completeBatch() 方法,但无论哪个方法,最终会调用到ProducerBatchdone() 方法,在done() 方法最终会调用到completeFutureAndFireCallbacks() 方法,该方法实现如下。

private void completeFutureAndFireCallbacks(
    long baseOffset,
    long logAppendTime,
    Function<Integer, RuntimeException> recordExceptions
) {
    produceFuture.set(baseOffset, logAppendTime, recordExceptions);

    for (int i = 0; i < thunks.size(); i++) {
        try {
            Thunk thunk = thunks.get(i);
            if (thunk.callback != null) {
                if (recordExceptions == null) {
                    // 没有异常时就通过FutureRecordMetadata拿到发送消息的元数据RecordMetadata
                    // 调用FutureRecordMetadata的value()方法是不阻塞的
                    RecordMetadata metadata = thunk.future.value();
                    // 然后将RecordMetadata送入回调函数
                    thunk.callback.onCompletion(metadata, null);
                } else {
                    // 有异常就拿到这个异常
                    RuntimeException exception = recordExceptions.apply(i);
                    // 然后将异常送入回调函数
                    thunk.callback.onCompletion(null, exception);
                }
            }
        } catch (Exception e) {
            log.error("Error executing user-provided callback on message for topic-partition '{}'", topicPartition, e);
        }
    }

    produceFuture.done();
}

至此消息发送后,回调函数就被执行了。

九. 同步和异步

使用KafkaProducersend() 方法发送消息时,对于KafkaProducer来说,整个的发送其实是完全异步的(除了拉取集群元数据信息)。

在业务代码中,调用了KafkaProducersend() 方法后,会得到FutureRecordMetadata,业务代码中可以通过决定是否调用FutureRecordMetadataget() 方法来实现同步或异步消息发送。

  1. 业务代码调用了FutureRecordMetadataget() 方法就是同步发送;
  2. 业务代码不调用FutureRecordMetadataget() 方法就是异步发送。

十. KafkaTemplate的消息发送模式

Spring中,提供了KafkaTemplate作为Kafka消息发送的客户端,其本质就是对KafkaProducer做了一层封装。KafkaTemplate提供了很多重载的send() 方法,这些方法最终会调用到KafkaTemplatedoSend() 方法,所以KafkaTemplate的消息发送,关键就在于KafkaTemplatedoSend() 方法的逻辑,代码实现如下。

protected ListenableFuture<SendResult<K, V>> doSend(final ProducerRecord<K, V> producerRecord) {
    final Producer<K, V> producer = getTheProducer(producerRecord.topic());
    this.logger.trace(() -> "Sending: " + KafkaUtils.format(producerRecord));
    // 这里的future会返回给业务代码
    final SettableListenableFuture<SendResult<K, V>> future = new SettableListenableFuture<>();
    Object sample = null;
    if (this.micrometerEnabled && this.micrometerHolder == null) {
        this.micrometerHolder = obtainMicrometerHolder();
    }
    if (this.micrometerHolder != null) {
        sample = this.micrometerHolder.start();
    }
    // 调用KafkaProducer来发送消息
    // 这里固定会传入一个回调函数
    // 这个回调函数很关键
    Future<RecordMetadata> sendFuture =
            producer.send(producerRecord, buildCallback(producerRecord, producer, future, sample));
    if (sendFuture.isDone()) {
        try {
            // 如果上面的isDone()方法立即就返回了true
            // 通常表明是消息发送快速失败了
            // 此时调用sendFuture的get()方法可以抛出异常信息
            // 从而业务代码可以感知到消息发送的异常
            sendFuture.get();
        }
        catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new KafkaException("Interrupted", e);
        }
        catch (ExecutionException e) {
            throw new KafkaException("Send failed", e.getCause());
        }
    }
    if (this.autoFlush) {
        flush();
    }
    this.logger.trace(() -> "Sent: " + KafkaUtils.format(producerRecord));
    // 这里返回的是在当前方法中创建出来的SettableListenableFuture
    // 而不是KafkaProducer返回的FutureRecordMetadata
    // 这里需要结合上面提到的回调函数一起理解
    return future;
}

KafkaTemplatedoSend() 方法实际就是调用到KafkaProducersend() 方法完成消息发送,但是区别在于,KafkaTemplatedoSend() 方法返回的是SettableListenableFuture,而KafkaProducersend() 方法返回的是FutureRecordMetadata,之所以KafkaTemplate这么干,是因为KafkaTemplate在调用KafkaProducersend() 方法时传入了一个回调函数,在这个回调函数中,会将消息元数据和异常写到SettableListenableFuture中,从而业务代码就可以通过SettableListenableFuture获取到消息元数据和异常,所以现在看一下buildCallback() 方法构建出了一个什么样的回调函数,如下所示。

// 这里的future就是业务代码调用KafkaTemplate的send()方法得到的future
// 这里的回调函数干的事情就是:
// 1. 如果消息发送没有异常则将消息元数据RecordMetadata写入future
// 2. 如果消息发送有异常则将异常信息写入future
// 这样业务代码中通过future就可以得到消息发送结果
private Callback buildCallback(final ProducerRecord<K, V> producerRecord, final Producer<K, V> producer, final SettableListenableFuture<SendResult<K, V>> future, @Nullable Object sample) {

    // 这里的metadata就是RecordMetadata
    // 这里exception就是发送消息时的异常
    return (metadata, exception) -> {
        try {
            if (exception == null) {
                if (sample != null) {
                    this.micrometerHolder.success(sample);
                }
                // 没有异常则将消息元数据写入到future
                // 业务代码通过future就能拿到消息元数据
                future.set(new SendResult<>(producerRecord, metadata));
                if (KafkaTemplate.this.producerListener != null) {
                    // 执行生产者监听器的onSuccess()逻辑
                    KafkaTemplate.this.producerListener.onSuccess(producerRecord, metadata);
                }
                KafkaTemplate.this.logger.trace(() -> "Sent ok: " + KafkaUtils.format(producerRecord)
                                                + ", metadata: " + metadata);
            }
            else {
                if (sample != null) {
                    this.micrometerHolder.failure(sample, exception.getClass().getSimpleName());
                }
                // 有异常则将异常写入到future
                // 业务代码通过future就能拿到异常
                future.setException(new KafkaProducerException(producerRecord, "Failed to send", exception));
                if (KafkaTemplate.this.producerListener != null) {
                    // 执行生产者监听器的onError()逻辑
                    KafkaTemplate.this.producerListener.onError(producerRecord, metadata, exception);
                }
                KafkaTemplate.this.logger.debug(exception, () -> "Failed to send: "
                                                + KafkaUtils.format(producerRecord));
            }
        }
        finally {
            if (!KafkaTemplate.this.transactional) {
                closeProducer(producer, false);
            }
        }
    };
}

也就是在ProducerBatch完成发送时,上述回调函数会执行,从而在回调函数中会将消息元数据或者异常写到SettableListenableFuture中,从而业务代码可以通过SettableListenableFuture拿到消息元数据或异常。

总结

Kafka生产者消息发送流程用下图进行总结。

Kafka-生产者发送消息流程图


大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈

你的名字-002