最新Kafka教程(包含kafka部署與基本操作、java連接kafka、spring連接kafka以及使用springboot)
歡迎轉載,轉載請註明網址:https://blog.csdn.net/qq_41910280
簡介:一篇全面Kafka教程。
文章目錄
1. Kafka概述
1.1 Kafka是什麼
Kafka是由Apache軟件基金會開發的一個開源流處理平臺,由Scala和Java編寫。Kafka是一種高吞吐量的分佈式發佈訂閱消息系統,它可以處理消費者規模的網站中的所有動作流數據。
1.2 Kafka有如下特性:
• 通過O(1)的磁盤數據結構提供消息的持久化,這種結構對於即使數以TB的消息存儲也能夠保持長時間的穩定性能。
• 高吞吐量:即使是非常普通的硬件Kafka也可以支持每秒數百萬的消息。
• 支持通過Kafka服務器和消費機集羣來分區消息。
• 支持Hadoop並行數據加載。
1.3 相關術語介紹
Broker: Kafka集羣包含一個或多個服務器,這種服務器被稱爲broker
Topic: 每條發佈到Kafka集羣的消息都有一個類別,這個類別被稱爲Topic。(物理上不同Topic的消息分開存儲,邏輯上一個Topic的消息雖然保存於一個或多個broker上但用戶只需指定消息的Topic即可生產或消費數據而不必關心數據存於何處)
Partition: Partition是物理上的概念,每個Topic包含一個或多個Partition.
Producer: 負責發佈消息到Kafka broker
Consumer: 消息消費者,向Kafka broker讀取消息的客戶端。
Consumer Group: 每個Consumer屬於一個特定的Consumer Group(可爲每個Consumer指定group name,若不指定group name則屬於默認的group)。
1.4 實現原理
始終記住, kafka集羣中每一個topic可以分佈在不同broker, 每個broker可以有多個partition(也就是說,每個topic可以有多個partition)。 而每個消費者group中每個消費者都會接管相應的一些partition, 即每個partition在每個consumer group中始終由同一個cosumer消費。
訂閱topic的不是consumer,而是consumer group。topic上的每一條消息分配到一個partition,然後由負責該partition的consumer去消費。
我們來看看官方的圖, 事實上server 1中也有P1 P2的備份,server 2也有P0 P3的備份。對於每一個partition,在同一個consumer group中總由同一個cosumer消費。另外,在一個topic中,每一個partition的消息是有序的(即生產者produce的消息和消費者consume的順序是一致的),而多個partition之間不保證順序。
傳統的消息隊列有隊列(點對點)和發佈/訂閱兩種模式。而在kafka中,每個topic都具有這兩種能力,如果有多個消費者組,那麼就是發佈/訂閱模式,如果所有消費者在同一個group就和隊列模式相似。並且每個topic都是可以擴展或者修改的,它可以是queuing也可以是publish/subscribe。
順序性:傳統消息隊列中每個topic的消息是順序保存在服務器上, 然後異步發送的消費者, 導致並行消費的消費者是無序消費的。傳統消息隊列通過“exclusive consumer”解決這個問題,這樣只有一個消費者在消費消息,只有這一個comsumer掛了才換到另一個consumer,因此失去了並行性。 而kafka每個分區僅有一個consumer消費,保證了其順序性,同時由於有多個分區,consumer可以均衡負載。建議消費者組中的消費者數量不要超過分區數量,否則部分consumer會打醬油。
1.5 kafka功能:
1.5.1 作爲消息系統
見1.4實現原理
補充:
傳統的消息隊列有消息確認機制, 當一條消息發送給cosumer時將它標記爲已發送未消費, 當消費者消費後發來確認再標記爲已消費。但是這種確認機制有兩個缺點, 一是comsumer在消費處理完成但發送確認之前故障, 則消息會被消費兩次, 二是性能問題, 消息隊列需要維護兩種狀態(已發送未消費、已消費)。因此考慮如何處理已發送但未確認的消息非常必要,這是一個棘手的問題。
而kafka採用offset替代消息確認機制。每個分區partition內的消息都是有序的,由同一個comsumer來消費,這使得狀態消耗非常小,每個partition只需要維護一個整數。同時還可以故意回到以前的offset重新消費消息。具體的,針對不同場景有三種消費模式:
- At-most-once(最多一次),
客戶端收到消息後,在處理消息前自動提交,這樣kafka就認爲consumer已經消費過了,偏移量增加。 - At-least-once(最少一次)
客戶端收到消息,處理消息,再提交反饋。這樣就可能出現消息處理完了,在提交反饋前,網絡中斷或者程序掛了,那麼kafka認爲這個消息還沒有被consumer消費,產生重複消息推送。 - Exactly-once(正好一次)
保證消息處理和提交反饋在同一個事務中,即有原子性。
具體實現參考https://blog.csdn.net/laojiaqi/article/details/79034798
https://my.oschina.net/chuibilong/blog/896685
1.5.2 作爲存儲系統
kafka具有持久化以及備份等機制, 可以作爲一種專用於高性能,低延遲提交日誌存儲,複製和傳播的專用分佈式文件系統。
1.5.3 用於流處理
在Kafka中,流處理器是指從輸入主題獲取連續的數據流,對這個輸入執行一些處理,併產生連續的數據流到輸出主題。
1.5.4 總結
這三個功能的組合似乎看起來不可思議, 但是它對kafka的作用至關重要。它允許kafka存儲和處理過去的歷史數據,也可以處理訂閱topic後的未來數據。同時kafka可以具有極低的延遲,也可以定期加載數據,甚至可以與長時間停機維護的離線系統集成。
1.6 發佈/訂閱模式與觀察者模式的區別
發佈/訂閱模式需要一個消息中間件,而消息的發佈者(publisher)和訂閱者(subscriber)不知道對方的存在,而觀察者模式則是Subject發生變化時及時告知Observer,使其做出響應,有push和pull兩種方式通知觀察者。
2. kafka部署與原生命令
下載後解壓到/opt/, 爲了寫教程我重新下載了最新穩定版本 (2019年4月11日) kafka_2.12-2.2.0, 下載地址
http://mirror.bit.edu.cn/apache/kafka/2.2.0/kafka_2.12-2.2.0.tgz
修改config/server.properties, 添加如下參數
# 修改了broker.id listener端口:前面可以加ip host.name和port已經過時
broker.id=0
# IP 端口 日誌存放路徑 zookeeper地址和端口 (記得註釋掉後面的這幾個參數)
listeners=PLAINTEXT://192.168.253.128:9092
log.dirs=/opt/kafka_2.12-2.2.0/logs
zookeeper.connect=localhost:2181
其中192.168.253.128是本機ip
kafka命令
(首先, 你需要啓動一個zookeeper,可以通過內置命令bin/zookeeper-server-start.sh config/zookeeper.properties啓動, 也可以和我一樣單獨啓動一個zookeeper)
啓動命令:
bin/kafka-server-start.sh config/server.properties
後臺啓動:
nohup bin/kafka-server-start.sh config/server.properties 1>/dev/null 2>&1 &
停止命令
bin/kafka-server-stop.sh
創建topic:
新版:bin/kafka-topics.sh --create --bootstrap-server 192.168.253.128:9092 --replication-factor 1 --partitions 1 --topic test
舊版(兼容):bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
查看所有topic:
新版:bin/kafka-topics.sh --list --bootstrap-server 192.168.253.128:9092
舊版(兼容):bin/kafka-topics.sh -list -zookeeper 127.0.0.1:2181
查看指定topic
新版:bin/kafka-topics.sh --describe --bootstrap-server 192.168.253.128:9092 --topic test
舊版(兼容):bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic test
修改分區的replication和partition
修改replication https://blog.csdn.net/lizhitao/article/details/45894109
修改partition https://www.cnblogs.com/buxizhizhoum/p/8251494.html
啓動producer:
bin/kafka-console-producer.sh --broker-list 192.168.253.128:9092 --topic test
啓動consumer:
bin/kafka-console-consumer.sh -bootstrap-server 192.168.253.128:9092 --topic test --from-beginning
(不加--from-beginning就是從未被consume的消息開始讀取數據)
刪除topic
1.bin/kafka-topics.sh --delete --zookeeper localhost:2181 --topic test
2.進入zookeeper目錄執行bin/zkCli.sh連接zookeeper, 然後執行rmr /brokers/topics/test
啓動producer和consumer之後你可以嘗試從控制檯發送和接收消息(小提示:在producer控制檯輸入” abcHdHc”並按下回車或兩下EOF(我也不知道爲什麼要按兩下…), 在consumer的控制檯上會顯示”abc”, ”^H”是鍵盤輸入的Backspace退格鍵), 你還可以嘗試使用kafka-connect從文件中獲取消息到kafka或者從kafka導出到文件,詳見http://kafka.apache.org/quickstart#quickstart_kafkaconnect
部署集羣
提示: 如果你的kafka自動退出, 你可以查看日誌, 如果有以下提示表明你的內存不夠用了
Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory(0x00000000c0000000, 1073741824, 0) failed; error='Cannot allocate memory' (errno=12)
#
# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (mmap) failed to map 1073741824 bytes for committing reserved memory.
你可以修改bin/kafka-server-start.sh, 將
if [ “x$KAFKA_HEAP_OPTS” = “x” ]; then
export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
fi
中的內存改爲-Xmx256M -Xms128M
> cp config/server.properties config/server-1.properties
> cp config/server.properties config/server-2.properties
> vi config/server-1.properties
broker.id=1
listeners=PLAINTEXT://192.168.253.128:9093
log.dirs=/opt/kafka_2.12-2.2.0/logs-1
zookeeper.connect=localhost:2181
> vi config/server-2.properties
broker.id=2
listeners=PLAINTEXT://192.168.253.128:9094
log.dirs=/opt/kafka_2.12-2.2.0/logs-2
zookeeper.connect=localhost:2181
> nohup bin/kafka-server-start.sh config/server-1.properties 1>/dev/null 2>&1 &
> nohup bin/kafka-server-start.sh config/server-2.properties 1>/dev/null 2>&1 &
# 創建一個3個replication, 1個partition的節點
> bin/kafka-topics.sh --create --bootstrap-server 192.168.253.128:9092 --replication-factor 3 --partitions 1 --topic my-replicated-topic
結果第一行是總覽, 之後各行分別是各個partition的情況, 其中leader表示主節點, replicas表示全部節點, isr(in-sync)表示狀態ok的節點
你可以kill其中的部分broker以測試其容錯性
Kafka Connect
之前我們使用的是從控制檯寫入數據並寫回控制檯, 現在我們來嘗試一下其他數據source與destination。
Kafka Connect是Kafka附帶的工具,可以向Kafka導入和導出數據。
首先, stop之前的kafka, 然後將ip改爲127.0.0.1重新啓動(或者修改後面的properties文件中的ip)
創建數據源
> echo -e "foo\nbar" > test.txt
執行以下命令(該命令讓source源連接器從test.txt讀取數據到主題connect-test, 然後sink接收器連接器從connect-test主題讀取數據寫入到test.sink.txt)
> bin/connect-standalone.sh config/connect-standalone.properties config/connect-file-source.properties config/connect-file-sink.properties
另起一個窗口, 依次執行
> more test.sink.txt
> echo Another line>> test.txt
> more test.sink.txt
Kafka Streams
(新版–bootstrap-server 192.168.253.128:9092和舊版–zookeeper localhost:2181可以互換, 後面不廢話了)
創建input topic
bin/kafka-topics.sh --create --bootstrap-server 127.0.0.1:9092 --replication-factor 1 --partitions 1 --topic streams-plaintext-input
創建output topic
bin/kafka-topics.sh --create --bootstrap-server 127.0.0.1:9092 --replication-factor 1 --partitions 1 --topic streams-wordcount-output
啓動wordcount demo
bin/kafka-run-class.sh org.apache.kafka.streams.examples.wordcount.WordCountDemo
在單獨的終端啓動input producer
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic streams-plaintext-input
啓動output comsumer
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092
–topic streams-wordcount-output
–from-beginning
–formatter kafka.tools.DefaultMessageFormatter
–property print.key=true
–property print.value=true
–property key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
–property value.deserializer=org.apache.kafka.common.serialization.LongDeserializer
在producer控制檯輸入all streams lead to kafka
在comsumer控制檯會打印
繼續在producer輸入hello kafka streams
comsumer控制檯會多出三行
這是如何做到的呢?
實際上, 有一個KTable<String, Long>, 每次出現同樣的String, 那麼Long就會+1, 並且是逐個單詞逐個單詞來處理的, 而不是一句一句來處理的, 比如說你接着輸入”kafka kafka kafka”, 控制檯出打印 而不僅是”kafka 5”
停止kafka streams
我們可以按照順序依次Ctrl + C掉comsumer、producer、wordcount、kafka、zookeeper
3. java連接kafka
先把kafka中server.properties的ip改回來, 然後啓動3個kafka
pom.xml
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.12</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
consumer
package com.example.consumer;
import java.util.Arrays;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
public class Consumer01 {
public static void main(String[] args) {
Properties props = new Properties();
// 定義kakfa 服務的地址,不需要將所有broker指定上
props.put("bootstrap.servers", "192.168.253.128:9092");
// 制定consumer group
props.put("group.id", "g1");
// 是否自動確認offset
props.put("enable.auto.commit", "true");
// 自動確認offset的時間間隔
props.put("auto.commit.interval.ms", "1000");
// key的序列化類
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// value的序列化類
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 定義consumer
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// 消費者訂閱的topic, 可同時訂閱多個
consumer.subscribe(Arrays.asList("test", "my-multipartition-topic"));
while (true) {
// 讀取數據,讀取超時時間爲100ms
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
}
}
producer
package com.example.provider;
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
public class Provider01 {
public static void main(String[] args) {
Properties props = new Properties();
// Kafka服務端的主機名和端口號
props.put("bootstrap.servers", "192.168.253.128:9092");
// 等待所有副本節點的應答 "-1"與"all"相同
props.put("acks", "all");
// 消息發送最大嘗試次數
props.put("retries", 0);
// 一批消息處理大小
props.put("batch.size", 16384);
// 請求延時
props.put("linger.ms", 1);
// 發送緩存區內存大小
props.put("buffer.memory", 33554432);
// key序列化
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// value序列化
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
for (int i = 0; i < 50; i++) {
System.out.println(i);
// producer.send(new ProducerRecord<String, String>("test", Integer.toString(i), "hello world->" + i));
producer.send(new ProducerRecord<String, String>("my-multipartition-topic", Integer.toString(i), "hello world->" + i));
}
System.out.println("producer close...");
producer.close();
}
}
通過上面的代碼,你已經可以嘗試發送消息和處理消息,還可以對比多個partition的topic和單個partition的結果,你會發現之前“特定partition中的消息是有序的,而多個partition的消息是無序的”這一結論的正確性。
callback producer
帶回調函數的producer
package com.example.provider;
import java.util.Properties;
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public class CallbackProvider {
public static void main(String[] args) throws InterruptedException {
Properties props = new Properties();
// Kafka服務端的主機名和端口號
// props.put("bootstrap.servers", "linux01:9092,linux02:9092,linux03:9092");
props.put("bootstrap.servers", "192.168.253.128:9092");
// 等待所有副本節點的應答
props.put("acks", "all");
// 消息發送最大嘗試次數
props.put("retries", 0);
// 一批消息處理大小
props.put("batch.size", 16384);
// 增加服務端請求延時
props.put("linger.ms", 1);
// 發送緩存區內存大小
props.put("buffer.memory", 33554432);
// key序列化
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// value序列化
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 自定義分區
// props.put("partitioner.class", "com.example.partitioner.Partitioner01");
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);
for (int i = 0; i < 50; i++) {
Thread.sleep(500);
kafkaProducer.send(new ProducerRecord<String, String>("my-multipartition-topic", "value->" + i), new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (metadata != null) {
System.out.println(metadata.partition() + "---" + metadata.offset());
}
}
});
}
kafkaProducer.close();
}
}
指定分區(放開上面provider “自定義分區”的註釋)
package com.example.partitioner;
import java.util.Map;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
public class Partitioner01 implements Partitioner {
@Override
public void configure(Map<String, ?> configs) {
}
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
// 控制分區
return 1;
}
@Override
public void close() {
}
}
producer攔截器
實現ProducerInterceptor接口, 裏面有4個方法(描述轉載至https://blog.csdn.net/u013256816/article/details/78573425):
- ProducerRecord<K, V> onSend(ProducerRecord<K, V> record):Producer在將消息序列化和分配分區之前會調用攔截器的這個方法來對消息進行相應的操作。一般來說最好不要修改消息ProducerRecord的topic、key以及partition等信息,如果要修改,也需確保對其有準確的判斷,否則會與預想的效果出現偏差。比如修改key不僅會影響分區的計算,同樣也會影響Broker端日誌壓縮(Log Compaction)的功能。
- void onAcknowledgement(RecordMetadata metadata, Exception exception):在消息被應答(Acknowledgement)之前或者消息發送失敗時調用,優先於用戶設定的Callback之前執行。這個方法運行在Producer的IO線程中,所以這個方法裏實現的代碼邏輯越簡單越好,否則會影響消息的發送速率。
- void close():關閉當前的攔截器,此方法主要用於執行一些資源的清理工作。
- configure(Map<String, ?> configs):用來初始化此類的方法,這個是ProducerInterceptor接口的父接口Configurable中的方法。
攔截器01: 在record的value值前面在上時間錯
package com.example.interceptor;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
/**
* 獲得record數據, 在value前面加上時間戳
*/
public class TimeInterceptor implements ProducerInterceptor<String, String> {
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
// 創建一個新的record,把時間戳寫入消息體的最前部
return new ProducerRecord<>(record.topic(), record.partition(), record.timestamp(),
record.key(), System.currentTimeMillis() + record.value());
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
攔截器02: 統計發送消息成功和發送失敗消息數,並在producer關閉時打印這兩個計數器
package com.example.interceptor;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
public class CountInterceptor implements ProducerInterceptor<String, String> {
private int errorCount = 0;
private int successCount = 0;
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
return record;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
// 統計失敗或者成功的次數
if (exception == null)
successCount++;
else
errorCount++;
}
@Override
public void close() {
// 保存結果
System.out.println("Successful sent: " + successCount);
System.out.println("Failed sent: " + errorCount);
}
@Override
public void configure(Map<String, ?> configs) {
}
}
修改Provider01: 添加攔截器配置
// 配置攔截器鏈
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, Arrays.asList("com.example.interceptor.CountInterceptor"
, "com.example.interceptor.TimeInterceptor"));
Kafka Streams
- 將輸入流” streams-plaintext-input”變成一個個單詞輸出到” streams-linesplit-output”
package com.example.streams;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.Topology;
import org.apache.kafka.streams.kstream.KStream;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
public class Linesplit {
public static void main(String[] args) {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-linesplit");// 唯一標識: 區別與kafka集羣通信的其他應用程序
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.253.128:9092");
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
final StreamsBuilder builder = new StreamsBuilder();
// 指定輸入和輸出的topic
KStream<String, String> source = builder.stream("streams-plaintext-input");
source.flatMapValues(value -> Arrays.asList(value.split("\\W+")))
.to("streams-linesplit-output");
final Topology topology = builder.build();
final KafkaStreams streams = new KafkaStreams(topology, props);
final CountDownLatch latch = new CountDownLatch(1);
// attach shutdown handler to catch control-c
Runtime.getRuntime().addShutdownHook(new Thread("streams-shutdown-hook") {
@Override
public void run() {
streams.close();
latch.countDown();
}
});
try {
streams.start();
latch.await();
} catch (Throwable e) {
System.exit(1);
}
System.exit(0);
}
}
啓動方式:
mvn clean package -Dmaven.test.skip=true
mvn exec:java -Dexec.mainClass=com.example.streams.Linesplit
按Ctrl+C退出streams
查看輸出主題record命令:
bin/kafka-console-consumer.sh --bootstrap-server 192.168.253.128:9092
–topic streams-linesplit-output
–from-beginning
–formatter kafka.tools.DefaultMessageFormatter
–property print.value=true
–property value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
2. WordCount單詞計數
package com.example.streams;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.Topology;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.Materialized;
import org.apache.kafka.streams.kstream.Produced;
import org.apache.kafka.streams.state.KeyValueStore;
import java.util.Arrays;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
public class WordCount {
public static void main(String[] args) {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-wordcount");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.253.128:9092");
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
final StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> source = builder.stream("streams-plaintext-input");
source.flatMapValues(value -> Arrays.asList(value.toLowerCase(Locale.getDefault()).split("\\W+")))
.groupBy((key, value) -> value)
.count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("counts-store"))
.toStream()
.to("streams-wordcount-output", Produced.with(Serdes.String(), Serdes.Long()));
final Topology topology = builder.build();
final KafkaStreams streams = new KafkaStreams(topology, props);
final CountDownLatch latch = new CountDownLatch(1);
// attach shutdown handler to catch control-c
Runtime.getRuntime().addShutdownHook(new Thread("streams-shutdown-hook") {
@Override
public void run() {
streams.close();
latch.countDown();
}
});
try {
streams.start();
latch.await();
} catch (Throwable e) {
System.exit(1);
}
System.exit(0);
}
}
啓動方式:
mvn clean package -Dmaven.test.skip=true
mvn exec:java -Dexec.mainClass=com.example.streams.WordCount
按Ctrl+C退出streams
查看輸出主題record命令:
bin/kafka-console-consumer.sh --bootstrap-server 192.168.253.128:9092
–topic streams-linesplit-output
–from-beginning
–formatter kafka.tools.DefaultMessageFormatter
–property print.value=true
–property value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
4. spring-kafka
- AutoCommitTest自動提交offset
- AckCommitTest手動提交offset
- ConfigTest使用java spring配置
請參考https://github.com/Spark4J/kafka-demo/tree/master/spring-kafka-demo,該項目遵循“Anti 996”協議,具體spring-kafka用法講解在下一章節“使用springboot”部分
5. 使用springboot
pom及源碼見上一章github項目
先來個quick start再逐一講解 (看不懂的跳過, 後面有講解)
application.yml
spring:
kafka:
bootstrap-servers: 192.168.253.128:9092
consumer:
group-id: group1
auto-offset-reset: earliest
enable-auto-commit: true
auto-commit-interval: 100
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
producer:
batch-size: 16384
retries: 3
acks: all
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
# transaction-id-prefix: myKafkaTransact
Appliaction.java
package com.example;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@SpringBootApplication
public class Application implements CommandLineRunner {
public static Logger logger = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Autowired
private KafkaTemplate<String, String> template;
private final CountDownLatch latch = new CountDownLatch(3);
@Override
public void run(String... args) throws Exception {
this.template.send("topic1", "foo1");
this.template.send("topic1", "foo2");
this.template.send("topic1", "foo3");
latch.await(60, TimeUnit.SECONDS);
logger.info("All received");
}
@KafkaListener(topics = "topic1")
public void listen(ConsumerRecord<?, ?> cr) throws Exception {
logger.info(cr.toString());
latch.countDown();
}
}
1.配置topic
首先需要定義KafkaAdmin, 它可以自動向kafka添加主題。然後可以使用NewTopic創建主題。
@Configuration
public class TopicManager {
@Value("${spring.kafka.bootstrap-servers}")
private String bootstarp_servers;
@Bean
public KafkaAdmin admin() {
Map<String, Object> configs = new HashMap<>();
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstarp_servers);
return new KafkaAdmin(configs);
}
@Bean
public NewTopic topic1() {
return new NewTopic("thing1", 10, (short) 2);
}
@Bean
public NewTopic topic2() {
return new NewTopic("thing2", 10, (short) 2);
}
}
2.發送消息
@Slf4j
@Component
public class MyProducer {
@Autowired
private KafkaTemplate<String, String> template;
// 異步非阻塞
public void sendToKafkaAsync(final ProducerRecord<String, String> record) {
ListenableFuture<SendResult<String, String>> future = template.send(record);
future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
@Override
public void onSuccess(SendResult<String, String> result) {
log.info("發送消息成功: record={}, result={}", record, result);
}
@Override
public void onFailure(Throwable ex) {
log.info("發送消息失敗: record={}, exception={}", record, ex.getMessage());
ex.printStackTrace();
}
});
}
// 同步阻塞
public void sendToKafkaSync(final ProducerRecord<String, String> record) {
try {
template.send(record).get(10, TimeUnit.SECONDS);
log.info("發送消息成功: record={}", record);
} catch (Exception e) {
log.info("發送消息失敗: record={}, exception={}", record, e.getMessage());
e.printStackTrace();
}
}
}
Test
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
@Autowired
MyProducer producer;
@Test
// @Transactional
public void contextLoads() {
long begin = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
// producer.sendToKafkaAsync(new ProducerRecord<>("topic1", "" + i, "val" + i));
producer.sendToKafkaSync(new ProducerRecord<>("topic1", "" + i, "val" + i));
}
long end = System.currentTimeMillis();
System.out.println("發送一百條消息花費的時間:" + (end - begin));// 異步53ms 同步289ms
}
}
事務
producer的冪等性: 當producer啓動時Kafka會爲每個producer分配一個PID(64位整數), producer發送的每條消息都有sequence number, 序列號從0開始遞增, 同時broker會爲每個producer保存其sequence number。這樣,每次producer發送新的消息batch時,如果攜帶sequence number和緩存的sequence number衝突(序號差大於一說明中間有數據未寫入broker, Producer 拋出 InvalidSequenceNumber; 序號小於一表示爲重複數據即ack回傳失敗, Producer 拋出 DuplicateSequenceNumber),則會拒絕寫入(重點:1.PID; 2. sequence number緩存更新機制)。注意: 每個partition的冪等性需要在同一個PID下, 單個Producer的同一個session中, 如果一個producer掛了被分配了新的PID則無法保證, 因此又有了事務。
producer的事務: 應用程序有一個transaction.id, 就算重啓也不會改變, 這種情況下kafka根據transaction.Id獲取對應的PID,這個對應關係是保存在事務日誌中。這樣可以確保相同的TransactionId返回相同的PID,用於恢復或者終止之前未完成的事務。同時每個producer註冊到kafka時還會初始化一個epoch,如果兩個producer具有相同的transaction.id, 其中epoch較老的視爲殭屍進程, kafka不接受其輸入。而消費者也需要將isolation.level設置爲read_committed。前面也說過,消費消息是通過offset來確定,而各topic-partition的offset記錄在名爲__consumer_offsets的topic中,即消息的消費也是向__consumer_offsets寫入消息,因此,原子性的向多個topic和partition寫入消息也保證了原子性的consume和produce。
詳見:https://juejin.im/post/5c00e1985188255125070ccb
https://blog.51cto.com/13739602/2161924
https://www.confluent.io/blog/transactions-apache-kafka/
https://www.confluent.io/blog/exactly-once-semantics-are-possible-heres-how-apache-kafka-does-it/
開啓事務:
第一種方式(不支持殭屍進程): 1.設置spring.kafka.producer.transaction-id-prefix 2.@Transactional
第二種方式: 將DefaultKafkaProducerFactory 的producerPerConsumerPartition設置爲false, 可以預防殭屍進程
3.接收消息
package com.example.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.annotation.PartitionOffset;
import org.springframework.kafka.annotation.TopicPartition;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;
@Slf4j
@Configuration
public class MyListener {
/*
這裏concurrency等於3, topicPartitions等於4,
那麼一個Container分配兩個partition, 另外兩個Container各分配一個partition
*/
@KafkaListener(id = "myListener"/*, topicPartitions =
{@TopicPartition(topic = "thing1", partitions = {"0", "1"}),
@TopicPartition(topic = "thing2", partitions = "0",
partitionOffsets = @PartitionOffset(partition = "1", initialOffset =
"100"))// Note: 不要在partitions和partitionOffsets中指定同一個分區
}*/, topics = "thing2", concurrency = "${listen.concurrency:3}")
public void listen(ConsumerRecord<?, ?> record/*, Acknowledgment acknowledgment 手動模式纔可以用*/) {
log.info("收到消息: {}", record);
}
}
參考文獻
- 官方文檔
神奇的小尾巴:
本人郵箱:[email protected] [email protected]
[email protected] 歡迎交流,共同進步。
歡迎轉載,轉載請註明本網址。