kafka的安裝與使用

陳晨辰~發表於2019-06-14

廣告系統設計與實現(九) 

9.1 Kafka 基礎知識

 

9.1.1 訊息系統

點對點訊息系統:生產者傳送一條訊息到queue,一個queue可以有很多消費者,但是一個訊息只能被一個消費者接受,當沒有消費者可用時,這個訊息會被儲存直到有 一個可用的消費者,所以Queue實現了一個可靠的負載均衡。

釋出訂閱訊息系統:釋出者傳送到topic的訊息,只有訂閱了topic的訂閱者才會收到訊息。topic實現了釋出和訂閱,當你釋出一個訊息,所有訂閱這個topic的服務都能得到這個訊息,所以從1到N個訂閱者都能得到這個訊息的拷貝。

 

9.1.2 kafka術語

訊息由producer產生,訊息按照topic歸類,併傳送到broker中,broker中儲存了一個或多個topic的訊息,consumer通過訂閱一組topic的訊息,通過持續的poll操作從broker獲取訊息,並進行後續的訊息處理。

Producer :訊息生產者,就是向broker髮指定topic訊息的客戶端。

Consumer :訊息消費者,通過訂閱一組topic的訊息,從broker讀取訊息的客戶端。

Broker :一個kafka叢集包含一個或多個伺服器,一臺kafka伺服器就是一個broker,用於儲存producer傳送的訊息。一個broker可以容納多個topic。

Topic :每條傳送到broker的訊息都有一個類別,可以理解為一個佇列或者資料庫的一張表。

Partition:一個topic的訊息由多個partition佇列儲存的,一個partition佇列在kafka上稱為一個分割槽。每個partition是一個有序的佇列,多個partition間則是無序的。partition中的每條訊息都會被分配一個有序的id(offset)。

 Offset:偏移量。kafka為每條在分割槽的訊息儲存一個偏移量offset,這也是消費者在分割槽的位置。kafka的儲存檔案都是按照offset.kafka來命名,位於2049位置的即為2048.kafka的檔案。比如一個偏移量是5的消費者,表示已經消費了從0-4偏移量的訊息,下一個要消費的訊息的偏移量是5。

Consumer Group (CG):若干個Consumer組成的集合。這是kafka用來實現一個topic訊息的廣播(發給所有的consumer)和單播(發給任意一個consumer)的手段。一個topic可以有多個CG。topic的訊息會複製(不是真的複製,是概念上的)到所有的CG,但每個CG只會把訊息發給該CG中的一個consumer。如果需要實現廣播,只要每個consumer有一個獨立的CG就可以了。要實現單播只要所有的consumer在同一個CG。用CG還可以將consumer進行自由的分組而不需要多次傳送訊息到不同的topic。

假如一個消費者組有兩個消費者,訂閱了一個具有4個分割槽的topic的訊息,那麼這個消費者組的每一個消費者都會消費兩個分割槽的訊息。消費者組的成員是動態維護的,如果新增或者減少了消費者組中的消費者,那麼每個消費者消費的分割槽的訊息也會動態變化。比如原來一個消費者組有兩個消費者,其中一個消費者因為故障而不能繼續消費訊息了,那麼剩下一個消費者將會消費全部4個分割槽的訊息。

 

9.1.3 kafka安裝和使用

在Windows安裝執行Kafkahttps://blog.csdn.net/weixin_38004638/article/details/91893910

 

9.1.4 kafka執行

一次寫入,支援多個應用讀取,讀取資訊是相同的

kafka-study.pom

<dependencies>
    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka_2.12</artifactId>
        <version>2.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-nop</artifactId>
        <version>1.7.24</version>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.0</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>

Producer生產者 

傳送訊息的方式,只管傳送,不管結果:只呼叫介面傳送訊息到 Kafka 伺服器,但不管成功寫入與否。由於 Kafka 是高可用的,因此大部分情況下訊息都會寫入,但在異常情況下會丟訊息
同步傳送:呼叫 send() 方法返回一個 Future 物件,我們可以使用它的 get() 方法來判斷訊息傳送成功與否
非同步傳送:呼叫 send() 時提供一個回撥方法,當接收到 broker 結果後回撥此方法

public class MyProducer {
    private static KafkaProducer<String, String> producer;
    //初始化
    static {
        Properties properties = new Properties();
        //kafka啟動,生產者建立連線broker的地址
        properties.put("bootstrap.servers", "127.0.0.1:9092");
        //kafka序列化方式
        properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        //自定義分割槽分配器
        properties.put("partitioner.class", "com.imooc.kafka.CustomPartitioner");
        producer = new KafkaProducer<>(properties);
    }

    /**
     * 建立topic:.\bin\windows\kafka-topics.bat --create --zookeeper localhost:2181
     * --replication-factor 1 --partitions 1 --topic kafka-study
     * 建立消費者:.\bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092
     * --topic imooc-kafka-study --from-beginning
     */
    //傳送訊息,傳送完後不做處理
    private static void sendMessageForgetResult() {
        ProducerRecord<String, String> record = new ProducerRecord<>("kafka-study", "name", "ForgetResult");
        producer.send(record);
        producer.close();
    }
    //傳送同步訊息,獲取傳送的訊息
    private static void sendMessageSync() throws Exception {
        ProducerRecord<String, String> record = new ProducerRecord<>("kafka-study", "name", "sync");
        RecordMetadata result = producer.send(record).get();
        System.out.println(result.topic());//imooc-kafka-study
        System.out.println(result.partition());//分割槽為0
        System.out.println(result.offset());//已傳送一條訊息,此時偏移量+1
        producer.close();
    }
    /**
     * 建立topic:.\bin\windows\kafka-topics.bat --create --zookeeper localhost:2181
     * --replication-factor 1 --partitions 3 --topic kafka-study-x
     * 建立消費者:.\bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092
     * --topic kafka-study-x --from-beginning
     */
    private static void sendMessageCallback() {
        ProducerRecord<String, String> record = new ProducerRecord<>("kafka-study-x", "name", "callback");
        producer.send(record, new MyProducerCallback());
        //傳送多條訊息
        record = new ProducerRecord<>("kafka-study-x", "name-x", "callback");
        producer.send(record, new MyProducerCallback());
        producer.close();
    }
    //傳送非同步訊息
    //場景:每條訊息傳送有延遲,多條訊息傳送,無需同步等待,可以執行其他操作,程式會自動非同步呼叫
    private static class MyProducerCallback implements Callback {
        @Override
        public void onCompletion(RecordMetadata recordMetadata, Exception e) {
            if (e != null) {
                e.printStackTrace();
                return;
            }
            System.out.println("*** MyProducerCallback ***");
            System.out.println(recordMetadata.topic());
            System.out.println(recordMetadata.partition());
            System.out.println(recordMetadata.offset());
        }
    }
    public static void main(String[] args) throws Exception {
        //sendMessageForgetResult();
        //sendMessageSync();
        sendMessageCallback();
    }
}

自定義分割槽分配器:決定訊息存放在哪個分割槽.。預設分配器使用輪詢存放,輪到已滿分割槽將會寫入失敗。

public class CustomPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes,
                         Object value, byte[] valueBytes, Cluster cluster) {
        //獲取topic所有分割槽
        List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic);
        int numPartitions = partitionInfos.size();
        //訊息必須有key
        if (null == keyBytes || !(key instanceof String)) {
            throw new InvalidRecordException("kafka message must have key");
        }
        //如果只有一個分割槽,即0號分割槽
        if (numPartitions == 1) {return 0;}
        //如果key為name,傳送至最後一個分割槽
        if (key.equals("name")) {return numPartitions - 1;}
        return Math.abs(Utils.murmur2(keyBytes)) % (numPartitions - 1);
    }
    @Override
    public void close() {}
    @Override
    public void configure(Map<String, ?> map) {}
}

啟動生產者傳送訊息,通過自定義分割槽分配器分配,查詢到topic資訊的value、partitioner

Kafka消費者(組)

* 自動提交位移 * 手動同步提交當前位移 * 手動非同步提交當前位移 * 手動非同步提交當前位移帶回撥 * 混合同步與非同步提交位移

public class MyConsumer {
    private static KafkaConsumer<String, String> consumer;
    private static Properties properties;
    //初始化
    static {
        properties = new Properties();
        //建立連線broker的地址
        properties.put("bootstrap.servers", "127.0.0.1:9092");
        //kafka反序列化
        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        //指定消費者組
        properties.put("group.id", "KafkaStudy");
    }

    //自動提交位移:由consume自動管理提交
    private static void generalConsumeMessageAutoCommit() {
        //配置
        properties.put("enable.auto.commit", true);
        consumer = new KafkaConsumer<>(properties);
        //指定topic
        consumer.subscribe(Collections.singleton("kafka-study-x"));
        try {
            while (true) {
                boolean flag = true;
                //拉取資訊,超時時間100ms
                ConsumerRecords<String, String> records = consumer.poll(100);
                //遍歷列印訊息
                for (ConsumerRecord<String, String> record : records) {
                    System.out.println(String.format(
                            "topic = %s, partition = %s, key = %s, value = %s",
                            record.topic(), record.partition(), record.key(), record.value()
                    ));
                    //訊息傳送完成
                    if (record.value().equals("done")) { flag = false; }
                }
                if (!flag) { break; }
            }
        } finally {
            consumer.close();
        }
    }

    //手動同步提交當前位移,根據需求提交,但容易傳送阻塞,提交失敗會進行重試直到丟擲異常
    private static void generalConsumeMessageSyncCommit() {
        properties.put("auto.commit.offset", false);
        consumer = new KafkaConsumer<>(properties);
        consumer.subscribe(Collections.singletonList("kafka-study-x"));
        while (true) {
            boolean flag = true;
            ConsumerRecords<String, String> records = consumer.poll(100);
            for (ConsumerRecord<String, String> record : records) {
                System.out.println(String.format(
                        "topic = %s, partition = %s, key = %s, value = %s",
                        record.topic(), record.partition(), record.key(), record.value()
                ));
                if (record.value().equals("done")) { flag = false; }
            }
            try {
                //手動同步提交
                consumer.commitSync();
            } catch (CommitFailedException ex) {
                System.out.println("commit failed error: " + ex.getMessage());
            }
            if (!flag) { break; }
        }
    }

    //手動非同步提交當前位移,提交速度快,但失敗不會記錄
    private static void generalConsumeMessageAsyncCommit() {
        properties.put("auto.commit.offset", false);
        consumer = new KafkaConsumer<>(properties);
        consumer.subscribe(Collections.singletonList("kafka-study-x"));
        while (true) {
            boolean flag = true;
            ConsumerRecords<String, String> records = consumer.poll(100);
            for (ConsumerRecord<String, String> record : records) {
                System.out.println(String.format(
                        "topic = %s, partition = %s, key = %s, value = %s",
                        record.topic(), record.partition(), record.key(), record.value()
                ));
                if (record.value().equals("done")) { flag = false; }
            }
            //手動非同步提交
            consumer.commitAsync();
            if (!flag) { break; }
        }
    }

    //手動非同步提交當前位移帶回撥
    private static void generalConsumeMessageAsyncCommitWithCallback() {
        properties.put("auto.commit.offset", false);
        consumer = new KafkaConsumer<>(properties);
        consumer.subscribe(Collections.singletonList("kafka-study-x"));
        while (true) {
            boolean flag = true;
            ConsumerRecords<String, String> records = consumer.poll(100);
            for (ConsumerRecord<String, String> record : records) {
                System.out.println(String.format(
                        "topic = %s, partition = %s, key = %s, value = %s",
                        record.topic(), record.partition(), record.key(), record.value()
                ));
                if (record.value().equals("done")) { flag = false; }
            }
            //使用java8函數語言程式設計
            consumer.commitAsync((map, e) -> {
                if (e != null) {
                    System.out.println("commit failed for offsets: " + e.getMessage());
                }
            });
            if (!flag) { break; }
        }
    }

    //混合同步與非同步提交位移
    @SuppressWarnings("all")
    private static void mixSyncAndAsyncCommit() {
        properties.put("auto.commit.offset", false);
        consumer = new KafkaConsumer<>(properties);
        consumer.subscribe(Collections.singletonList("kafka-study-x"));
        try {
            while (true) {
                //boolean flag = true;
                ConsumerRecords<String, String> records = consumer.poll(100);
                for (ConsumerRecord<String, String> record : records) {
                    System.out.println(String.format(
                            "topic = %s, partition = %s, key = %s, " + "value = %s",
                            record.topic(), record.partition(),
                            record.key(), record.value()
                    ));
                    //if (record.value().equals("done")) { flag = false; }
                }
                //手動非同步提交,保證效能
                consumer.commitAsync();
                //if (!flag) { break; }
            }
        } catch (Exception ex) {
            System.out.println("commit async error: " + ex.getMessage());
        } finally {
            try {
                //非同步提交失敗,再嘗試手動同步提交
                consumer.commitSync();
            } finally {
                consumer.close();
            }
        }
    }

    public static void main(String[] args) {
        //自動提交位移
        generalConsumeMessageAutoCommit();
        //手動同步提交當前位移
        //generalConsumeMessageSyncCommit();
        //手動非同步提交當前位移
        //generalConsumeMessageAsyncCommit();
        //手動非同步提交當前位移帶回撥
        //generalConsumeMessageAsyncCommitWithCallback()
        //混合同步與非同步提交位移
        //mixSyncAndAsyncCommit();
    }
}

先啟動消費者等待接收訊息,再啟動生產者傳送訊息,進行消費訊息

 

相關文章