文章目录

  • 引言
  • 一)发送消息的流程解释:
  • 二)发送消息的代码阅读:
  • 1.1 examples.Producer 入口阅读:
  • 1.2 Producer.doSend()
  • 1.2.1 第一步的获取元数据waitOnMetadata()方法解释
  • 1.2.2 第三步分区器的解释
  • 1.2.3 第四步消息大小的计算
  • 1.2.4 第七步消息放入缓冲区(重要的内容)
  • 1.2.4.1 缓冲区双端队列介绍
  • 1.2.4.2 缓冲区ByteBufferPool介绍
  • 1.2.4.3 append方法解析
  • 三)Producer的初始化的代码阅读:
  • 2.1 org.apache.kafka.clients.producer.KafkaProducer
  • 2.1.1 Sender线程的启动。



引言

围绕Producer的学习,主要分为两个方面:

  • Producer如何初始化;
  • Producer.send() 是如何工作的 ;

入门指南

Kafka 是 Java 和 Scala 语言混合的项目,目前 IDEA 对这种项目的支持度最好,想要学习源码的话推荐用这个 IDE,省去很多解决环境问题的时间
开始阅读服务端代码的话,推荐从两个地方开始:

KafkaApis 这个类是各种外部请求 Handler 的入口,从这里能看到 Kafka 各种接口的实现
KafkaServer 这个类是服务端 Broker 对应的实现类,从这里作为入口能学习到整个服务端启动的流程

阅读客户端代码的话,Producer 可以关注 RecordAccumulator 这个类,这是解耦本地消息缓存和网络发送线程的枢纽
Consumer 应该关注 ConsumerCoordinator 的实现,关于消费者组管理客户端的实现都在这里,想了解重平衡等客户端核心机制都需要在这里找答案。

kafka生产端消息重复发送 kafka 发送消息例子_学习

一)发送消息的流程解释:

1 整个生产者客户端由两个线程进行协调运行 ,这两个线程分别是: 主线程sender 线程 。
2 随后通过可能的拦截器、序列化器、分区器的作用,缓存至RecordAccumulator [əˈkjuːmjəleɪtə®] 。
3 Sender线程RecordAccumulator中获取消息,发送至Kafka中。

kafka生产端消息重复发送 kafka 发送消息例子_学习_02


RecordAccumulator 主要用来缓存消息,以便Sender线程可以批量发送,进而减少网络传输。在其内部,为每个分区都维护了一个双端队列,队列中的内容就是ProducerBatch,结构为:Deque<ProducerBatch>。消息从尾巴追加进入,sender线程从队列头部获取。

ProducerBatch 并不是ProducerRecord , 每一个ProducerBatch中至少包含一个ProducerRecord。你可以理解为ProducerBatch是一个批次,而ProducerRecord是一条消息。

BufferPool :在RecordAccumulator中,为了复用发送消息要用到的byteBuff,而规避掉频繁创建与释放byteBuff,所以对该对象进行了池化。不过ByteBufferPool只针对特定大小的ByteBuff进行管理,默认是16KB。

ProducerBatch写入的过程: 当ProducerRecord流入RecordAccumulator时,会先寻找与消息分区对应的双端队列(如果没有则新创建),查看Producer中是否可以写入这条消息。如果可以写入则对该批次进行一次追加写入,如果无法写入则创建一个新的ProducerBatch。

在新建ProducerBatch的时候,会评估消息的大小是否超过了batchSize;
如果没有超过batchSize的大小,呢么以batchSize的大小进行创建,这样再使用完这块内存区域后,可以通过BufferPool进行管理;
如果超过了batchSize的大小,则以评估消息的大小作为创建宜居,呢么这块内存区域不会再被复用了。

Sender线程拉取RecordAccumulator: 当拉取成功后,会进一步将原本的<分区,Deque<Producer>>转换成<Node,List<ProducerBatch>> 的形式;
其中Node表示Kafka集群的broker节点。

对于网络连接而言,生产者客户端是与具体的broker节点建立的建立,是向broker节点发送消息,并不关系是哪一个分区;
对于KafkaProducer的应用逻辑而言,我们是关系向哪个分区发送哪些消息,所以这里面会存在上述转换,其本质是应用层逻辑转述到网络IO层面。

<Node,List<ProducerBatch>> <Node,Request> ,sender线程会将上述的Node,List<ProducerBatch> 进行一次转换,以具备协议请求的request对象向kafka发送。

InFlightRequests : 请求在从Sender线程发往kafka之前,还会保存一份到InFightRequests中;
InFightRequests中,是以Map<NodeId,Deque<Request>>进行保存的,其目的是缓存呢些已经发出去但是还是没有收到相应的请求。
InFightRequests中,还提供许多管理类的方式,比如具体可以缓存多少条发送出去但是未得到相应的消息。

以上为一个较粗的流程,弱化掉了MATEDATA的相关内容,主要是我怕我乱了。

实际上在创建 KafkaProducer 实例时,该 Sender 线程开始运行时首先会创建与 Broker 的连接,并换取元数据信息。


二)发送消息的代码阅读:

1.1 examples.Producer 入口阅读:

这里是一个简易的Producer 发送消息的代码,以此作为入口。

kafka.examples.Producer.java

public class Producer extends Thread {

    private final KafkaProducer<Integer, String> producer;
    private final String topic;
    private final Boolean isAsync;

    /**
     * 构建方法,初始化生产者对象
     * @param topic
     * @param isAsync
     */
    public Producer(String topic, Boolean isAsync) {
        Properties props = new Properties();
        // 用户拉取kafka的元数据
        props.put("bootstrap.servers", "localhost:9092");
        props.put("client.id", "DemoProducer");
        //设置序列化的类。
        //二进制的格式
        props.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        //消费者,消费数据的时候,就需要进行反序列化。
        //TODO 初始化kafkaProducer
        producer = new KafkaProducer<>(props);
        this.topic = topic;
        this.isAsync = isAsync;
    }

    public void run() {
        int messageNo = 1;
        // 一直会往kafka发送数据
        while (true) {
            String messageStr = "Message_" + messageNo;
            long startTime = System.currentTimeMillis();
            //构建消息体
            ProducerRecord<Integer, String> record = new ProducerRecord<>(topic, messageNo, messageStr);
            //isAsync , kafka发送数据的时候,有两种方式
            //1: 异步发送
            //2: 同步发送
            //isAsync: true的时候是异步发送,false就是同步发送
            if (isAsync) { // Send asynchronously
                //异步发送,一直发送,消息响应结果交给回调函数处理
                //这样的方式,性能比较好,我们生产代码用的就是这种方式。
                producer.send(record, new DemoCallBack(startTime, messageNo, messageStr));
            } else { // Send synchronously
                try {
                    //同步发送
                    //发送一条消息,等这条消息所有的后续工作都完成以后才继续下一条消息的发送。
                    producer.send(record).get();
                    System.out.println("Sent message: (" + messageNo + ", " + messageStr + ")");
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            }
            ++messageNo;
        }
    }
}

/**
 * org.apache.kafka.clients.producer.Callback
 * 注意这里是Kafka提供的回调类,不是juc的。
 */
class DemoCallBack implements Callback {

    private final long startTime;
    private final int key;
    private final String message;

    public DemoCallBack(long startTime, int key, String message) {
        this.startTime = startTime;
        this.key = key;
        this.message = message;
    }

    /**
     * A callback method the user can implement to provide asynchronous handling of request completion. This method will
     * be called when the record sent to the server has been acknowledged. Exactly one of the arguments will be
     * non-null.
     *
     * @param metadata  The metadata for the record that was sent (i.e. the partition and offset). Null if an error
     *                  occurred.
     * @param exception The exception thrown during processing of this record. Null if no error occurred.
     */
    public void onCompletion(RecordMetadata metadata, Exception exception) {
        long elapsedTime = System.currentTimeMillis() - startTime;

        if (exception != null) {
            System.out.println("有异常");
            //一般我们生产里面 还会有其它的备用的链路。
        } else {
            System.out.println("说明没有异常信息,成功的!!");
        }
        if (metadata != null) {
            System.out.println(
                    "message(" + key + ", " + message + ") sent to partition(" + metadata.partition() +
                            "), " +
                            "offset(" + metadata.offset() + ") in " + elapsedTime + " ms");
        } else {
            exception.printStackTrace();
        }
    }
}

1.2 Producer.doSend()

org.apache.kafka.clients.producer.KafkaProducer#doSend()

这里是具体发送消息的代码,流程如下:








同步等待拉取元数据

对消息的key和value进行序列化

根据分区器选择消息应该发送的分区

根据元数据信息封装分区对象

给每一条消息都绑定他的回调函数

把消息放入accumulator

唤醒sender线程


Sender线程为什么这里需要被wakeup,暂且空着

/**
     * Implementation of asynchronously send a record to a topic.
     */
    private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
        TopicPartition tp = null;
        try {
            /**
             * 步骤一:
             *      同步等待拉取元数据。
             *  maxBlockTimeMs 最多能等待多久。
             */
            ClusterAndWaitTime clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
            //clusterAndWaitTime.waitedOnMetadataMs 代表的是拉取元数据用了多少时间。
            //maxBlockTimeMs -用了多少时间 = 还剩余多少时间可以使用。
            long remainingWaitMs = Math.max(0, maxBlockTimeMs - clusterAndWaitTime.waitedOnMetadataMs);
            //更新集群的元数据
            Cluster cluster = clusterAndWaitTime.cluster;
            /**
             * 步骤二:
             *  对消息的key和value进行序列化。
             */
            byte[] serializedKey;
            try {
                serializedKey = keySerializer.serialize(record.topic(), 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");
            }
            byte[] serializedValue;
            try {
                serializedValue = valueSerializer.serialize(record.topic(), 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");
            }
            /**
             * 步骤三:
             *  根据分区器选择消息应该发送的分区。
             *
             *  因为前面我们已经获取到了元数据
             *  这儿我们就可以根据元数据的信息
             *  计算一下,我们应该要把这个数据发送到哪个分区上面。
             */
            int partition = partition(record, serializedKey, serializedValue, cluster);

            int serializedSize = Records.LOG_OVERHEAD + Record.recordSize(serializedKey, serializedValue);
            /**
             * 步骤四:
             *  确认一下消息的大小是否超过了最大值。
             *  KafkaProducer初始化的时候,指定了一个参数,代表的是Producer这儿最大能发送的是一条消息能有多大
             *  默认最大是1M,我们一般都回去修改它。
             */
            ensureValidRecordSize(serializedSize);
            /**
             * 步骤五:
             *  根据元数据信息,封装分区对象
             */
            tp = new TopicPartition(record.topic(), partition);
            long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp();
            log.trace("Sending record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition);
            // producer callback will make sure to call both 'callback' and interceptor callback
            /**
             * 步骤六:
             *  给每一条消息都绑定他的回调函数。因为我们使用的是异步的方式发送的消息。
             */
            Callback interceptCallback = this.interceptors == null ? callback : new InterceptorCallback<>(callback, this.interceptors, tp);
            /**
             * 步骤七:
             *  把消息放入accumulator(32M的一个内存)
             *  然后有accumulator把消息封装成为一个批次一个批次的去发送。
             */
            RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, interceptCallback, remainingWaitMs);
            //如果批次满了
            //或者新创建出来一个批次
            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);
                /**
                 * 步骤八:
                 *  唤醒sender线程。他才是真正发送数据的线程。
                 *  还记得之前说到的Accumulator中的双端队列吗? 
                 * 	sender线程从头部拉取消息,这里就是对sender线程进行一次wakeUp
                 *  主线程只要保证消息投递到Accumulator即可。
                 */
                this.sender.wakeup();
            }
            return result.future;
            // handling exceptions and record the errors;
            // for API exceptions return them in the future,
            // for other exceptions throw directly
        } catch (ApiException e) {
            log.debug("Exception occurred during message send:", e);
            if (callback != null)
                callback.onCompletion(null, e);
            this.errors.record();
            if (this.interceptors != null)
                this.interceptors.onSendError(record, tp, e);
            return new FutureFailure(e);
        } catch (InterruptedException e) {
            this.errors.record();
            if (this.interceptors != null)
                this.interceptors.onSendError(record, tp, e);
            throw new InterruptException(e);
        } catch (BufferExhaustedException e) {
            this.errors.record();
            this.metrics.sensor("buffer-exhausted-records").record();
            if (this.interceptors != null)
                this.interceptors.onSendError(record, tp, e);
            throw e;
        } catch (KafkaException e) {
            this.errors.record();
            if (this.interceptors != null)
                this.interceptors.onSendError(record, tp, e);
            throw e;
        } catch (Exception e) {
            // we notify interceptor about all exceptions, since onSend is called before anything else in this method
            if (this.interceptors != null)
                this.interceptors.onSendError(record, tp, e);
            throw e;
        }

    }
1.2.1 第一步的获取元数据waitOnMetadata()方法解释

ClusterAndWaitTime clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);

org.apache.kafka.clients.Metadata

/**
     * Wait for metadata update until the current version is larger than the last version we know of
     */
    public synchronized void awaitUpdate(final int lastVersion, final long maxWaitMs) throws InterruptedException {
        if (maxWaitMs < 0) {
            throw new IllegalArgumentException("Max time to wait for metadata updates should not be < 0 milli seconds");
        }
        //获取当前时间
        long begin = System.currentTimeMillis();
        //看剩余可以使用的时间,一开始是最大等待的时间。
        long remainingWaitMs = maxWaitMs;
        //version是元数据的版本号。
        //如果当前的这个version小于等于上一次的version。
        //说明元数据还没更新。
        //因为如果sender线程那儿 更新元数据,如果更新成功了,sender线程肯定回去累加这个version。
        while (this.version <= lastVersion) {
            //如果还有剩余的时间。
            if (remainingWaitMs != 0)
                //主线程休眠
                //这儿被唤醒有两个情况:要么获取到元数据了,被sender线程唤醒,要么就是时间到了。
                //唤醒的代码见org.apache.kafka.clients.Metadata#update()
                wait(remainingWaitMs);
            //如果代码执行到这儿 说明就要么就被唤醒了,要么就到点了。
            //计算一下花了多少时间。
            long elapsed = System.currentTimeMillis() - begin;
            //已经超时了
            if (elapsed >= maxWaitMs)
                //报一个超时的异常。
                throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
            //再次计算 可以使用的时间。
            remainingWaitMs = maxWaitMs - elapsed;
        }
    }

在发送消息中获取元数据这一步,当前线程进行了wait() ;这个点记住,等会回答。

1.2.2 第三步分区器的解释

org.apache.kafka.clients.producer.internals#DefaultPartitioner#partition

/**
     * Compute the partition for the given record.
     *
     * @param topic The topic name
     * @param key The key to partition on (or null if no key)
     * @param keyBytes serialized key to partition on (or null if no key)
     * @param value The value to partition on or null
     * @param valueBytes serialized value to partition on or null
     * @param cluster The current cluster metadata
     */
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        //首先获取到我们要发送消息的对应的topic的分区的信息
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        //计算出来分区的总的个数
        int numPartitions = partitions.size();

        /**
         * producer 发送数据的时候:
         *  message
         *
         *  key,message
         *  message
         *
         */

        //策略一: 如果发送消息的时候,没有指定key
        if (keyBytes == null) {
            //这儿有一个计数器
            //每次执行都会加一
            //10

            int nextValue = counter.getAndIncrement();
            //获取可用的分区数
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() > 0) {
                //计算我们要发送到哪个分区上。
                //一个数如果对10进行取模, 0-9之间
                //11 % 9
                //12 % 9
                //13 % 9
                //实现一个轮训的效果,能达到负载均衡。
                int part = Utils.toPositive(nextValue) % availablePartitions.size();
                //根据这个值分配分区好。
                return availablePartitions.get(part).partition();
            } else {
                // no partitions are available, give a non-available partition
                return Utils.toPositive(nextValue) % numPartitions;
            }
        } else {
            //策略二:这个地方就是指定了key
            // hash the keyBytes to choose a partition
            //直接对key取一个hash值 % 分区的总数取模
            //如果是同一个key,计算出来的分区肯定是同一个分区。
            //如果我们想要让消息能发送到同一个分区上面,那么我们就
            //必须指定key. 这一点非常重要 。
            //公司二方库包装的KafkaProducer就是用这种方式的
            
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }
1.2.3 第四步消息大小的计算
/**
     * Validate that the record size isn't too large
     */
    private void ensureValidRecordSize(int size) {
        //如果一条消息的大小超过了 1M,那么就会报错
        if (size > this.maxRequestSize)
            //自定义了异常。
            throw new RecordTooLargeException("The message is " + size +
                    " bytes when serialized which is larger than the maximum request size you have configured with the " +
                    ProducerConfig.MAX_REQUEST_SIZE_CONFIG +
                    " configuration.");
        //如果你的一条消息的大小超过32M也会报错。
        if (size > this.totalMemorySize)
            throw new RecordTooLargeException("The message is " + size +
                    " bytes when serialized which is larger than the total memory buffer you have configured with the " +
                    ProducerConfig.BUFFER_MEMORY_CONFIG +
                    " configuration.");
    }
1.2.4 第七步消息放入缓冲区(重要的内容)
1.2.4.1 缓冲区双端队列介绍

我们在第五步中,已经将消息调整为<话题,分区> 的数据格式了

/**
 * 步骤五:
 *  根据元数据信息,封装分区对象
 */
tp = new TopicPartition(record.topic(), partition);

kafka生产端消息重复发送 kafka 发送消息例子_kafka生产端消息重复发送_03

话题与双端队列的关系:一个话题如果具备N个分区,则现在会形成N个双端队列,既话题的每一个分区会有一个双端队列;


kafka生产端消息重复发送 kafka 发送消息例子_元数据_04



1.2.4.2 缓冲区ByteBufferPool介绍

每个RecordBatch的如果用时申请内存,呢么一定是一个频繁创建且频繁销毁的内存使用场景,Kafka对其进行了池化的优化。

ByterBuffPool中缓存了与RecordBatch大小相等的byteBuffer。当需要使用ByteBuffer时从池中获取,使用完毕做归还,进而避免了ByteBuffer频繁创建的过程。


1.2.4.3 append方法解析

org.apache.kafka.clients.producer.internals.RecordAccumulator#append

/**
     * Add a record to the accumulator, return the append result
     * <p>
     * The append result will contain the future metadata, and flag for whether the appended batch is full or a new batch is created
     * <p>
     *
     * @param tp The topic/partition to which this record is being sent
     * @param timestamp The timestamp of the record
     * @param key The key for the record
     * @param value The value for the record
     * @param callback The user-supplied callback to execute when the request is complete
     * @param maxTimeToBlock The maximum time in milliseconds to block for buffer memory to be available
     */
    public  RecordAppendResult append(TopicPartition tp,
                                     long timestamp,
                                     byte[] key,
                                     byte[] value,
                                     Callback callback,
                                     long maxTimeToBlock) throws InterruptedException {
        // We keep track of the number of appending thread to make sure we do not miss batches in
        // abortIncompleteBatches().
        appendsInProgress.incrementAndGet();
        try {
            // check if we have an in-progress batch
            /**
             * 步骤一:先根据分区找到应该插入到哪个队列里面。
             * 如果有已经存在的队列,那么我们就使用存在队列
             * 如果队列不存在,那么我们新创建一个队列
             *
             * 我们肯定是有了存储批次的队列,但是大家一定要知道一个事
             * 我们代码第一次执行到这儿,获取其实就是一个空的队列。
             *
             * 现在代码第二次执行进来。
             * 假设 分区还是之前的那个分区。
             *
             * 这个方法里面我们之前分析,里面就是针对batchs进行的操作
             * 里面kafka自己封装了一个数据结构:CopyOnWriteMap (这个数据结构本来就是线程安全的)
             *
             *
             * 根据当前消息的信息,获取一个队列,也可以说每有一条消息都要获取一次队列
             */
            Deque<RecordBatch> dq = getOrCreateDeque(tp);
            /**
             * 假设我们现在有线程一,线程二,线程三
             *
             */
            synchronized (dq) {
                if (closed)
                    throw new IllegalStateException("Cannot send after the producer is closed.");
                /**
                 * 步骤二:
                 *      尝试往队列里面的批次里添加数据
                 *
                 *      一开始添加数据肯定是失败的,我们目前只是创建了队列
                 *      数据是需要存储在批次对象里面(这个批次对象是需要分配内存的)
                 *      我们目前还没有分配内存,所以如果按场景驱动的方式,
                 *      代码第一次运行到这儿其实是不成功的。
                 */
                RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);
                //第一次进来的时候appendResult的值就为null
                if (appendResult != null)
                    return appendResult;
            }





            // we don't have an in-progress record batch try to allocate a new batch
            /**
             * 步骤三:计算一个批次的大小
             * 在消息的大小和批次的大小之间取一个最大值,用这个值作为当前这个批次的大小。
             * 有可能我们的一个消息的大小比一个设定好的批次的大小还要大。
             * 默认一个批次的大小是16K。
             * 所以我们看到这段代码以后,应该给我们一个启示。
             * 如果我们生产者发送数的时候,如果我们的消息的大小都是超过16K,
             * 说明其实就是一条消息就是一个批次,那也就是说消息是一条一条被发送出去的。
             * 那如果是这样的话,批次这个概念的设计就没有意义了
             * 所以大家一定要根据自定公司的数据大小的情况去设置批次的大小。
             *
             *
             *
             */
            int size = Math.max(this.batchSize, Records.LOG_OVERHEAD + Record.recordSize(key, value));
            log.trace("Allocating a new {} byte message buffer for topic {} partition {}", size, tp.topic(), tp.partition());
            /**
             * 步骤四:
             *  根据批次的大小去分配内存
             *
 		     *	这里分配内存,在多线程场景下会为”同一个“批次申请多次内存。
 		     *	但在后续代码创建队列追加批次后, if (appendResult != null)
 		     *	又再次释放掉。
             */
            ByteBuffer buffer = free.allocate(size, maxTimeToBlock);

            synchronized (dq) {
                // Need to check if producer is closed again after grabbing the dequeue lock.
                if (closed)
                    throw new IllegalStateException("Cannot send after the producer is closed.");
                /**
                 * 步骤五:
                 *      尝试把数据写入到批次里面。
                 *      代码第一次执行到这儿的时候 依然还是失败的(appendResult==null)
                 *      目前虽然已经分配了内存
                 *      但是还没有创建批次,那我们向往批次里面写数据
                 *      还是不能写的。
                 *
                 *   线程二进来执行这段代码的时候,是成功的。
                 */
                RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);
                //失败的意思就是appendResult 还是会等于null
                if (appendResult != null) {
                    //释放内存,对应前面创建了多个内存的场景。
                    free.deallocate(buffer);
                    return appendResult;
                }
                /**
                 * 步骤六:
                 *  根据内存大小封装批次
                 *
                 *
                 *  线程一到这儿 会根据内存封装出来一个批次。
                 */
                MemoryRecords records = MemoryRecords.emptyRecords(buffer, compression, this.batchSize);
                RecordBatch batch = new RecordBatch(tp, records, time.milliseconds());
                //尝试往这个批次里面写数据,到这个时候 我们的代码会执行成功。

                FutureRecordMetadata future = Utils.notNull(batch.tryAppend(timestamp, key, value, callback, time.milliseconds()));
                /**
                 * 步骤七:
                 *  把这个批次放入到这个队列的队尾
                 *
                 */
                dq.addLast(batch);
                incomplete.add(batch);
                return new RecordAppendResult(future, dq.size() > 1 || batch.records.isFull(), true);
            }//释放锁
        } finally {
            appendsInProgress.decrementAndGet();
        }
    }

RecordAccumulator#getOrCreateDeque(TopicPartition tp)

/**
     * Get the deque for the given topic-partition, creating it if necessary.
     */
    private Deque<RecordBatch> getOrCreateDeque(TopicPartition tp) {

        /**
         * CopyonWriteMap:
         *      get
         *      put
         *
         */
        //直接从batches里面获取当前分区对应的存储队列

        Deque<RecordBatch> d = this.batches.get(tp);
        //代码第一次执行到这里是获取不到队列的,也就是说d这个变量的值为null
        if (d != null)
            return d;
        //代码继续执行,创建出来一个新的空队列,
        d = new ArrayDeque<>();
        //把这个空的队列存入batches 这个数据结构里面
        Deque<RecordBatch> previous = this.batches.putIfAbsent(tp, d);
        if (previous == null)
            return d;
        else
            //直接返回新的结果
            return previous;
    }

RecordAccumulator#tryAppend(long timestamp, byte[] key, byte[] value, Callback callback, Deque deque)

/**
     * If `RecordBatch.tryAppend` fails (i.e. the record batch is full), close its memory records to release temporary
     * resources (like compression streams buffers).
     */
    private RecordAppendResult tryAppend(long timestamp, byte[] key, byte[] value, Callback callback, Deque<RecordBatch> deque) {
        //首先要获取到队列里面一个批次
        RecordBatch last = deque.peekLast();
        //第一次进来是没有批次的,所以last肯定为null

        //线程二进来的时候,这个last不为空
        if (last != null) {
            //线程二就插入数据就ok了
            FutureRecordMetadata future = last.tryAppend(timestamp, key, value, callback, time.milliseconds());
            if (future == null)
                last.records.close();
            else
                //返回值就不为null了
                return new RecordAppendResult(future, deque.size() > 1 || last.records.isFull(), false);
        }
        //返回结果就是一个null值
        return null;
    }

三)Producer的初始化的代码阅读:

我们摘要下关键的初始化属性:

  • partitioner 分区器
  • interceptorList 过滤器链
  • metadata 元数据

内部结构如下

kafka生产端消息重复发送 kafka 发送消息例子_kafka_05

  • compressionType 消息压缩方式
  • accumulator 消息缓冲区
  • NetworkClient 网络连接池
  • Sender 线程的初始化;

2.1 org.apache.kafka.clients.producer.KafkaProducer

org.apache.kafka.clients.producer.KafkaProducer

@SuppressWarnings({"unchecked", "deprecation"})
    private KafkaProducer(ProducerConfig config, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
        try {
            log.trace("Starting the Kafka producer");
            // 配置一些用户自定义的参数
            Map<String, Object> userProvidedConfigs = config.originals();
            this.producerConfig = config;
            this.time = new SystemTime();
            //配置clientId
            clientId = config.getString(ProducerConfig.CLIENT_ID_CONFIG);
            if (clientId.length() <= 0)
                clientId = "producer-" + PRODUCER_CLIENT_ID_SEQUENCE.getAndIncrement();
            Map<String, String> metricTags = new LinkedHashMap<String, String>();
            metricTags.put("client-id", clientId);
            //metric一些东西,我们一般分析源码的时候 不需要去关心
            MetricConfig metricConfig = new MetricConfig().samples(config.getInt(ProducerConfig.METRICS_NUM_SAMPLES_CONFIG))
                    .timeWindow(config.getLong(ProducerConfig.METRICS_SAMPLE_WINDOW_MS_CONFIG), TimeUnit.MILLISECONDS)
                    .tags(metricTags);
            List<MetricsReporter> reporters = config.getConfiguredInstances(ProducerConfig.METRIC_REPORTER_CLASSES_CONFIG,
                    MetricsReporter.class);
            reporters.add(new JmxReporter(JMX_PREFIX));
            this.metrics = new Metrics(metricConfig, reporters, time);

            //设置分区器
            this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);
            /**
             *
             * Producer发送消息的时候,我们代码里面一般会设置重试机制的
             *
             * 分布式,网络 不稳定
             * Producer 指定 重试机制
             */
            //重试时间 retry.backoff.ms 默认100ms
            long retryBackoffMs = config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG);
            //设置序列化器
            if (keySerializer == null) {
                this.keySerializer = config.getConfiguredInstance(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
                        Serializer.class);
                this.keySerializer.configure(config.originals(), true);
            } else {
                config.ignore(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG);
                this.keySerializer = keySerializer;
            }
            if (valueSerializer == null) {
                this.valueSerializer = config.getConfiguredInstance(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                        Serializer.class);
                this.valueSerializer.configure(config.originals(), false);
            } else {
                config.ignore(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG);
                this.valueSerializer = valueSerializer;
            }

            // load interceptors and make sure they get clientId
            //
            userProvidedConfigs.put(ProducerConfig.CLIENT_ID_CONFIG, clientId);
            //设置拦截器
            //类似于一个过滤器
            List<ProducerInterceptor<K, V>> interceptorList = (List) (new ProducerConfig(userProvidedConfigs)).getConfiguredInstances(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
                    ProducerInterceptor.class);
            this.interceptors = interceptorList.isEmpty() ? null : new ProducerInterceptors<>(interceptorList);

            ClusterResourceListeners clusterResourceListeners = configureClusterResourceListeners(keySerializer, valueSerializer, interceptorList, reporters);
            //生产者从服务端那儿拉取过来的kafka的元数据。
            //生产者要想去拉取元数据, 发送网络请求,重试,
            //metadata.max.age.ms(默认5分钟)
            //生产者每隔一段时间都要去更新一下集群的元数据。
            this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG), true, clusterResourceListeners);
            //max.request.size 生产者往服务端发送消息的时候,规定一条消息最大多大?
            //如果你超过了这个规定消息的大小,你的消息就不能发送过去。
            //默认是1M,这个值偏小,在生产环境中,我们需要修改这个值。
            //经验值是10M。但是大家也可以根据自己公司的情况来。
            this.maxRequestSize = config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG);
            //指的是缓存大小
            //默认值是32M,这个值一般是够用,如果有特殊情况的时候,我们可以去修改这个值。
            this.totalMemorySize = config.getLong(ProducerConfig.BUFFER_MEMORY_CONFIG);
            //kafka是支持压缩数据的,这儿设置压缩格式。
            //提高你的系统的吞吐量,你可以设置压缩格式。
            //一次发送出去的消息就更多。生产者这儿会消耗更多的cpu.
            this.compressionType = CompressionType.forName(config.getString(ProducerConfig.COMPRESSION_TYPE_CONFIG));
            /* check for user defined settings.
             * If the BLOCK_ON_BUFFER_FULL is set to true,we do not honor METADATA_FETCH_TIMEOUT_CONFIG.
             * This should be removed with release 0.9 when the deprecated configs are removed.
             */
            if (userProvidedConfigs.containsKey(ProducerConfig.BLOCK_ON_BUFFER_FULL_CONFIG)) {
                log.warn(ProducerConfig.BLOCK_ON_BUFFER_FULL_CONFIG + " config is deprecated and will be removed soon. " +
                        "Please use " + ProducerConfig.MAX_BLOCK_MS_CONFIG);
                boolean blockOnBufferFull = config.getBoolean(ProducerConfig.BLOCK_ON_BUFFER_FULL_CONFIG);
                if (blockOnBufferFull) {
                    this.maxBlockTimeMs = Long.MAX_VALUE;
                } else if (userProvidedConfigs.containsKey(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG)) {
                    log.warn(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG + " config is deprecated and will be removed soon. " +
                            "Please use " + ProducerConfig.MAX_BLOCK_MS_CONFIG);
                    this.maxBlockTimeMs = config.getLong(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG);
                } else {
                    this.maxBlockTimeMs = config.getLong(ProducerConfig.MAX_BLOCK_MS_CONFIG);
                }
            } else if (userProvidedConfigs.containsKey(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG)) {
                log.warn(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG + " config is deprecated and will be removed soon. " +
                        "Please use " + ProducerConfig.MAX_BLOCK_MS_CONFIG);
                this.maxBlockTimeMs = config.getLong(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG);
            } else {
                this.maxBlockTimeMs = config.getLong(ProducerConfig.MAX_BLOCK_MS_CONFIG);
            }

            /* check for user defined settings.
             * If the TIME_OUT config is set use that for request timeout.
             * This should be removed with release 0.9
             */
            if (userProvidedConfigs.containsKey(ProducerConfig.TIMEOUT_CONFIG)) {
                log.warn(ProducerConfig.TIMEOUT_CONFIG + " config is deprecated and will be removed soon. Please use " +
                        ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG);
                this.requestTimeoutMs = config.getInt(ProducerConfig.TIMEOUT_CONFIG);
            } else {
                this.requestTimeoutMs = config.getInt(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG);
            }
            //TODO 创建了一个核心的组件
            this.accumulator = new RecordAccumulator(config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
                    this.totalMemorySize,
                    this.compressionType,
                    config.getLong(ProducerConfig.LINGER_MS_CONFIG),
                    retryBackoffMs,
                    metrics,
                    time);

            List<InetSocketAddress> addresses = ClientUtils.parseAndValidateAddresses(config.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG));
            //去更新元数据
            //addresses 这个地址其实就是我们写producer代码的时候,传参数的时候,传进去了一个broker的地址。
            //所以这段代码看起来像是去服务端拉取元数据,所以我们去验证一下,是否真的去拉取元数据。
            //TODO update方法初始化的时候并没有去服务端拉取元数据。
            this.metadata.update(Cluster.bootstrap(addresses), time.milliseconds());
            ChannelBuilder channelBuilder = ClientUtils.createChannelBuilder(config.values());
            //TODO 初始化了一个重要的管理网路的组件。
            /**
             *  (1)connections.max.idle.ms: 默认值是9分钟
             *      一个网络连接最多空闲多久,超过这个空闲时间,就关闭这个网络连接。
             *
             *  (2)max.in.flight.requests.per.connection:默认是5
             *    producer向broker发送数据的时候,其实是有多个网络连接。
             *    每个网络连接可以忍受 producer端发送给broker 消息然后消息没有响应的个数。
             *
             *
             *    因为kafka有重试机制,所以有可能会造成数据乱序,如果想要保证有序,这个值要把设置为1.
             *
             * (3)send.buffer.bytes:socket发送数据的缓冲区的大小,默认值是128K
             * (4)receive.buffer.bytes:socket接受数据的缓冲区的大小,默认值是32K。
              */
            NetworkClient client = new NetworkClient(
                    new Selector(config.getLong(ProducerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG), this.metrics, time, "producer", channelBuilder),
                    this.metadata,
                    clientId,
                    config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION),
                    config.getLong(ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG),
                    config.getInt(ProducerConfig.SEND_BUFFER_CONFIG),
                    config.getInt(ProducerConfig.RECEIVE_BUFFER_CONFIG),
                    this.requestTimeoutMs, time);
            //
            //我们在项目中一般都会去设置重试,
            /**
             *
             * (1) retries:重试的次数
             * (2) acks:
             *   0:
             *      producer发送数据到broker后,就完了,没有返回值,不管写成功还是写失败都不管了。
             *   1:
             *      producer发送数据到broker后,数据成功写入leader partition以后返回响应。
             *   -1:
             *       producer发送数据到broker后,数据要写入到leader partition里面,并且数据同步到所有的
             *       follower partition里面以后,才返回响应。
             *
             */
            //这个就是一个线程
            this.sender = new Sender(client,
                    this.metadata,
                    this.accumulator,
                    config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION) == 1,
                    config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG),
                    (short) parseAcks(config.getString(ProducerConfig.ACKS_CONFIG)),
                    config.getInt(ProducerConfig.RETRIES_CONFIG),
                    this.metrics,
                    new SystemTime(),
                    clientId,
                    this.requestTimeoutMs);
            String ioThreadName = "kafka-producer-network-thread" + (clientId.length() > 0 ? " | " + clientId : "");

            //创建了一个线程,然后里面传进去了一个sender对象。
            //把业务的代码和关于线程的代码给隔离开来。
            //关于线程的这种代码设计的方式,其实也值得大家积累的。
            this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
            //启动线程。
            this.ioThread.start();

            this.errors = this.metrics.sensor("errors");
            config.logUnused();
            AppInfoParser.registerAppInfo(JMX_PREFIX, clientId);
            log.debug("Kafka producer started");
        } catch (Throwable t) {
            // call close methods if internal objects are already constructed
            // this is to prevent resource leak. see KAFKA-2121
            close(0, TimeUnit.MILLISECONDS, true);
            // now propagate the exception
            throw new KafkaException("Failed to construct kafka producer", t);
        }
    }

2.1.1 Sender线程的启动。

kafka生产端消息重复发送 kafka 发送消息例子_学习_06

org.apache.kafka.clients.producer.KafkaProducer#KafkaProducer(ProducerConfig config, Serializer<K> keySerializer, Serializer<V> valueSerializer)

在上述构造方法中,我们摘录下ioThread的构建与启动:

this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
//启动线程。
this.ioThread.start();

之前说过Producer构建成功后,就会创建连接。
这里的Sender线程被run了,呢么具体的执行逻辑里面应该包含连接建立的相关内容。

org.apache.kafka.clients.producer.internals.Sender#run

/**
     * The main run loop for the sender thread
     */
    public void run() {
        log.debug("Starting Kafka producer I/O thread.");

        //其实代码就是一个死循环,然后一直在运行。
        //所以我们要知道sender线程启动起来一以后是一直在运行的。
        while (running) {
            try {
                //TODO 核心代码
                run(time.milliseconds());
            } catch (Exception e) {
                log.error("Uncaught error in kafka producer I/O thread: ", e);
            }
        }

        log.debug("Beginning shutdown of Kafka producer I/O thread, sending remaining records.");

        // okay we stopped accepting requests but there may still be
        // requests in the accumulator or waiting for acknowledgment,
        // wait until these are completed.
        while (!forceClose && (this.accumulator.hasUnsent() || this.client.inFlightRequestCount() > 0)) {
            try {
                run(time.milliseconds());
            } catch (Exception e) {
                log.error("Uncaught error in kafka producer I/O thread: ", e);
            }
        }
        if (forceClose) {
            // We need to fail all the incomplete batches and wake up the threads waiting on
            // the futures.
            this.accumulator.abortIncompleteBatches();
        }
        try {
            this.client.close();
        } catch (Exception e) {
            log.error("Failed to close network client", e);
        }

        log.debug("Shutdown of Kafka producer I/O thread has completed.");
    }

org.apache.kafka.clients.producer.internals.Sender#run(long now)

/**
     * Run a single iteration of sending
     * 
     * @param now
     *            The current POSIX time in milliseconds
     */
    void run(long now) {

        /**
         *
         *  (1)代码第一次进来:
         *  获取元数据,因为我们是根据场景驱动的方式,目前是我们第一次代码进来,还没有获取到元数据
         *  所以这个cluster里面是没有元数据,如果这儿没有元数据的话,这个方法里面接下来的代码就不用看了
         *  因为接下来的这些代码都依赖这个元数据。
         *
         *  (2)代码第二次进来:
         *  我们用场景驱动的方式,现在我们的代码是第二次进来
         *  第二次进来的时候,已经有元数据了,所以cluster这儿是有元数据。
         *
         * 步骤一:
         *      获取元数据
         *
         *   这个方法就是我们今天晚上主要分析到一个方法了
         *   我们先大概了看一下里面有哪些功能?
         *
         *
         *   场景驱动方式
         *   获取到元数据
         */
        Cluster cluster = metadata.fetch();
        // get the list of partitions with data ready to send
        /**
         * 步骤二:
         *      首先是判断哪些partition有消息可以发送:
         *        我们看一下一个批次可以发送出去的条件
         *
         *      获取到这个partition的leader partition对应的broker主机(根据元数据信息来就可以了)
         *
         *      哪些broker上面需要我们去发送消息?
         */
        RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);

        /**
         * 步骤三:
         *      标识还没有拉取到元数据的topic
         */
        if (!result.unknownLeaderTopics.isEmpty()) {
            // The set of topics with unknown leader contains topics with leader election pending as well as
            // topics which may have expired. Add the topic again to metadata to ensure it is included
            // and request metadata update, since there are messages to send to the topic.
            for (String topic : result.unknownLeaderTopics)
                this.metadata.add(topic);
            this.metadata.requestUpdate();
        }

        // remove any nodes we aren't ready to send to
        Iterator<Node> iter = result.readyNodes.iterator();
        long notReadyTimeout = Long.MAX_VALUE;
        while (iter.hasNext()) {
            Node node = iter.next();
            /**
             * 步骤四:检查与要发送数据的主机的网络是否已经建立好。
             */
                if (!this.client.ready(node, now)) {

                    //如果返回的是false  !false 代码就进来
                    //移除result 里面要发送消息的主机。
                    //所以我们会看到这儿所有的主机都会被移除
                iter.remove();
                notReadyTimeout = Math.min(notReadyTimeout, this.client.connectionDelay(node, now));
            }
        }

        /**
         * 步骤五:
         *
         * 我们有可能要发送的partition有很多个,
         * 很有可能有一些partition的leader partition是在同一台服务器上面。
         *  假设我们集群只有3台服务器 0  1 2
         *  主题:p0 p1 p2 p3
         *
         * p0:leader -> 0
         * p1:leader -> 0
         * p2:leader -> 1
         * p3:leader -> 2
         *
         * 当我们的分区的个数大于集群的节点的个数的时候,一定会有多个leader partition在同一台服务器上面。
         *
         * 按照broker进行分组,同一个broker的partition为同一组
         * 0:{p0,p1}  -> 批次
         * 1:{p2}
         * 2:{p3}
         *
         * 一个批次就一个请求  -> broker
         *
         * 减少网络传输到次数
         *
         *
         */

        //所以我们发现 如果网络没有建立的话,这儿的代码是不执行的
        Map<Integer, List<RecordBatch>> batches = this.accumulator.drain(cluster,
                                                                         result.readyNodes,
                                                                         this.maxRequestSize,
                                                                         now);
        if (guaranteeMessageOrder) {
            // Mute all the partitions drained
            //如果batches 空的话,这而的代码也就不执行了。
            for (List<RecordBatch> batchList : batches.values()) {
                for (RecordBatch batch : batchList)
                    this.accumulator.mutePartition(batch.topicPartition);
            }
        }
        /**
         * 步骤六:
         *  对超时的批次是如何处理的?
         *
         */
        List<RecordBatch> expiredBatches = this.accumulator.abortExpiredBatches(this.requestTimeout, now);
        // update sensors
        for (RecordBatch expiredBatch : expiredBatches)
            this.sensors.recordErrors(expiredBatch.topicPartition.topic(), expiredBatch.recordCount);

        sensors.updateProduceRequestMetrics(batches);
        /**
         * 步骤七:
         *      创建发送消息的请求
         *
         *
         * 创建请求
         * 我们往partition上面去发送消息的时候,有一些partition他们在同一台服务器上面
         * ,如果我们一分区一个分区的发送我们网络请求,那网络请求就会有一些频繁
         * 我们要知道,我们集群里面网络资源是非常珍贵的。
         * 会把发往同个broker上面partition的数据 组合成为一个请求。
         * 然后统一一次发送过去,这样子就减少了网络请求。
         */

        //如果网络连接没有建立好 batches其实是为空。
        //也就说其实这段代码也是不会执行。


        List<ClientRequest> requests = createProduceRequests(batches, now);
        // If we have any nodes that are ready to send + have sendable data, poll with 0 timeout so this can immediately
        // loop and try sending more data. Otherwise, the timeout is determined by nodes that have partitions with data
        // that isn't yet sendable (e.g. lingering, backing off). Note that this specifically does not include nodes
        // with sendable data that aren't ready to send since they would cause busy looping.
        long pollTimeout = Math.min(result.nextReadyCheckDelayMs, notReadyTimeout);
        if (result.readyNodes.size() > 0) {
            log.trace("Nodes with data ready to send: {}", result.readyNodes);
            log.trace("Created {} produce requests: {}", requests.size(), requests);
            pollTimeout = 0;
        }
        //TODO 发送请求的操作
        for (ClientRequest request : requests)
            //绑定 op_write
            client.send(request, now);

        /**
         * 解接下来要发送网络请求了,把把数据写到服务端?
         *
         * Selector
         *
         * write
         *
         */

        // if some partitions are already ready to be sent, the select time would be 0;
        // otherwise if some partition already has some data accumulated but not ready yet,
        // the select time will be the time difference between now and its linger expiry time;
        // otherwise the select time will be the time difference between now and the metadata expiry time;
        //TODO 重点就是去看这个方法
        //就是用这个方法拉取的元数据。

        /**
         * 步骤八:
         * 真正执行网络操作的都是这个NetWordClient这个组件
         * 包括:发送请求,接受响应(处理响应)
         *
         * 拉取元数据信息,靠的就是这段代码
         */
        //我们猜这儿可能就是去建立连接。
        this.client.poll(pollTimeout, now);
    }

org.apache.kafka.clients.producer.internals.Sender#run#this.client.poll(pollTimeout, now)

上述为真正建立连接的方法,其实现为

org.apache.kafka.clients.NetworkClient#poll

/**
     * Do actual reads and writes to sockets.
     *
     * @param timeout The maximum amount of time to wait (in ms) for responses if there are none immediately,
     *                must be non-negative. The actual timeout will be the minimum of timeout, request timeout and
     *                metadata timeout
     * @param now The current time in milliseconds
     * @return The list of responses received
     */
    @Override
    public List<ClientResponse> poll(long timeout, long now) {
    
        /**
         * 在这个方法里面有涉及到kafka的网络的方法
         */
         
        //步骤一:封装了一个要拉取元数据请求 ,˙这一步有点难,我们先跳过//todo
        long metadataTimeout = metadataUpdater.maybeUpdate(now);
        try {


            //步骤二: 发送请求,进行复杂的网络操作
            //TODO 执行网络IO的操作。  NIO
            this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs));
        } catch (IOException e) {
            log.error("Unexpected error during I/O", e);
        }

        // process completed actions
        long updatedNow = this.time.milliseconds();
        List<ClientResponse> responses = new ArrayList<>();
        handleCompletedSends(responses, updatedNow);
        //步骤三:处理响应,响应里面就会有我们需要的元数据。
        /**
         * 这个地方是生产者是如何获取元数据.
         * 其实Kafak获取元数据的流程跟我们发送消息的流程是一模一样。
         * 获取元数据 -》 判断网络连接是否建立好 -》 建立网络连接
         * -》 发送请求(获取元数据的请求) -》 服务端发送回来响应(带了集群的元数据信息)
         *
         */
        handleCompletedReceives(responses, updatedNow);

        handleDisconnections(responses, updatedNow);
        handleConnections();
        //TODO 处理长时间没有接受到响应
        handleTimedOutRequests(responses, updatedNow);

        // invoke callbacks
        for (ClientResponse response : responses) {
            if (response.request().hasCallback()) {
                try {
                    //调用的响应的里面的我们之前发送出去的请求的回调函数
                    //看到了这儿,我们回头再去看一下
                    //我们当时发送请求的时候,是如何封装这个请求。
                    //不过虽然目前我们还没看到,但是我们可以大胆猜一下。
                    //当时封装网络请求的时候,肯定是给他绑定了一个回调函数。
                    response.request().callback().onComplete(response);
                } catch (Exception e) {
                    log.error("Uncaught error in request completion:", e);
                }
            }
        }

        return responses;
    }

呢么之前存疑的wait方法是在哪里被唤醒的呢?

org.apache.kafka.clients.Metadata#update()

/**
     * Updates the cluster metadata. If topic expiry is enabled, expiry time
     * is set for topics if required and expired topics are removed from the metadata.
     */
    public synchronized void update(Cluster cluster, long now) {
        Objects.requireNonNull(cluster, "cluster should not be null");

        this.needUpdate = false;
        this.lastRefreshMs = now;
        this.lastSuccessfulRefreshMs = now;
        this.version += 1;
        //这个默认值是true
        if (topicExpiryEnabled) {
            // Handle expiry of topics from the metadata refresh set.
            //但是我们目前topics是空的
            //所以下面的代码是不会被运行的。

            //到现在我们的代码是不是第二次进来了呀?
            //如果第二次进来,此时此刻进来,我们 producer.send(topics,)方法
            //要去拉取元数据  -》 sender -》 代码走到的这儿。
            //第二次进来的时候,topics其实不为空了,因为我们已经给它赋值了
            //所以这儿的代码是会继续运行的。
            for (Iterator<Map.Entry<String, Long>> it = topics.entrySet().iterator(); it.hasNext(); ) {
                Map.Entry<String, Long> entry = it.next();
                long expireMs = entry.getValue();
                if (expireMs == TOPIC_EXPIRY_NEEDS_UPDATE)
                    entry.setValue(now + TOPIC_EXPIRY_MS);
                else if (expireMs <= now) {
                    it.remove();
                    log.debug("Removing unused topic {} from the metadata list, expiryMs {} now {}", entry.getKey(), expireMs, now);
                }
            }
        }

        for (Listener listener: listeners)
            listener.onMetadataUpdate(cluster);

        String previousClusterId = cluster.clusterResource().clusterId();
        //这个的默认值是false,所以这个分支的代码不会被运行。
        if (this.needMetadataForAllTopics) {
            // the listener may change the interested topics, which could cause another metadata refresh.
            // If we have already fetched all topics, however, another fetch should be unnecessary.
            this.needUpdate = false;
            this.cluster = getClusterForCurrentTopics(cluster);
        } else {
            //所以代码执行的是这儿。
            //直接把刚刚传进来的对象赋值给了这个cluster。
            //cluster代表的是kafka集群的元数据。
            //初始化的时候,update这个方法没有去服务端拉取数据。
            this.cluster = cluster;//address
        }

        // The bootstrap cluster is guaranteed not to have any useful information
        if (!cluster.isBootstrapConfigured()) {
            String clusterId = cluster.clusterResource().clusterId();
            if (clusterId == null ? previousClusterId != null : !clusterId.equals(previousClusterId))
                log.info("Cluster ID: {}", cluster.clusterResource().clusterId());
            clusterResourceListeners.onUpdate(cluster.clusterResource());
        }
        //大家发现这儿会有一个notifyAll,这个最重要的一个作用是不是就是唤醒,我们上一讲
        //看到那个wait的线程。
        notifyAll();
        log.debug("Updated cluster metadata version {} to {}", this.version, this.cluster);
    }

以上为第一次记录Kafka消息发送相关代码 ;