關於kafka stream的介紹此處不再多做介紹,可以參考別的博客。直接看代碼。
第一個stream應用程序Pipe
創建一個pipe類:
public class Pipe {
public static void main(String[] args) throws Exception {
}
}
編寫Streams應用程序的第一步是創建一個java.util.Properties映射,以指定StreamsConfig中定義的不同Streams執行配置值。 需要設置幾個重要的配置值:StreamsConfig.BOOTSTRAP_SERVERS_CONFIG(用於指定用於建立與Kafka集羣的初始連接的主機/端口對的列表)和StreamsConfig.APPLICATION_ID_CONFIG(用於提供Streams的唯一標識符) 應用程序,以便與與同一個Kafka羣集通信的其他應用程序區分開來:
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-pipe");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
此外,可以在同一映射中自定義其他配置,例如,記錄鍵值對的默認序列化和反序列化庫:
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
接下來,我們將定義Streams應用程序的計算邏輯。 在Kafka Streams中,此計算邏輯被定義爲連接的處理器節點的拓撲。 我們可以使用拓撲構建器來構建這樣的拓撲,
final StreamsBuilder builder = new StreamsBuilder();
然後使用以下拓撲生成器從名爲streams-plaintext-input的Kafka topic創建源流:
KStream<String, String> source = builder.stream("streams-plaintext-input");
現在,我們得到了一個KStream,它從其源Kafka主題streams-plaintext-input連續生成記錄。 記錄被組織爲String類型的鍵值對。 我們可以使用此流執行的最簡單的操作是將其寫入另一個Kafka主題,即名爲streams-pipe-output
source.to("streams-pipe-output");
請注意,我們還可以將上述兩行連接爲一行,如下所示:
builder.stream("streams-plaintext-input").to("streams-pipe-output");
通過執行以下操作,我們可以檢查從此builder創建的拓撲類型:
final Topology topology = builder.build();
並將其描述打印爲標準輸出爲:
System.out.println(topology.describe());
此時,如果我們編譯並運行該程序,它將輸出以下信息:
Topologies:
Sub-topology: 0
Source: KSTREAM-SOURCE-0000000000 (topics: [streams-plaintext-input])
--> KSTREAM-SINK-0000000001
Sink: KSTREAM-SINK-0000000001 (topic: streams-pipe-output)
<-- KSTREAM-SOURCE-0000000000
從上面的打印結果當中,說明了構造的拓撲具有兩個處理器節點,即源節點KSTREAM-SOURCE-0000000000和宿節點KSTREAM-SINK-0000000001。 KSTREAM-SOURCE-0000000000連續從Kafka主題流-明文輸入中讀取記錄,並將它們通過管道傳輸到其下游節點KSTREAM-SINK-0000000001; KSTREAM-SINK-0000000001會將接收到的每個記錄寫入另一個Kafka topic streams-pipe-output(->和<-箭頭指示此節點的下游和上游處理器節點,即“子級”和“父級”)。它還說明了這種簡單的拓撲沒有與之關聯的全局狀態存儲。
請注意,當我們在代碼中構建拓撲時,我們始終可以在任何給定的點上像上面一樣描述拓撲,因此作爲用戶,可以交互地“嘗試”拓撲中定義的計算邏輯,直到對它滿意爲止。假設我們已經完成了這種簡單的拓撲結構,即以一種無限的流方式將數據從一個Kafka主題傳送到另一個主題,現在我們可以使用上面剛剛構建的兩個組件來構造Streams客戶端:指定的java.util.Properties實例和Topology
對象。
final KafkaStreams streams = new KafkaStreams(topology, props);
通過調用其start()函數,我們可以觸發此客戶端的執行。 在此客戶端上調用close()之前,執行不會停止。 例如,我們可以添加一個帶有倒計時閂鎖的關閉hook ,以捕獲用戶中斷並在終止該程序時關閉客戶端:
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);
截止到目前我們的代碼長這個樣子
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.Properties;
import java.util.concurrent.CountDownLatch;
public class Pipe {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-pipe");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost: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.to("streams-pipe-output");
final Topology topology = builder.build();
//System.out.println(topology.describe());
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);
}
}
如果已經在localhost:9092上啓動並運行了Kafka代理,並且在該代理上創建了主題streams-plaintext-input和streams-pipe-output,則可以使用Maven在IDE或命令行中運行此代碼。 :
> mvn clean package
> mvn exec:java -Dexec.mainClass=myapps.Pipe
第二個stream應用程序Line Split
我們已經介紹瞭如何使用兩個關鍵組件構造Streams客戶端:StreamsConfig和Topology。 現在,讓我們繼續通過擴展當前Topology來添加一些實際的處理邏輯。 我們可以先複製現有的Pipe.java類來創建另一個程序:LineSplit
並更改其類名以及應用程序id config以與原始程序區分開:
public class LineSplit {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-linesplit");
// ...
}
}
由於每個源流的記錄都是字符串類型的鍵值對,因此我們將值字符串視爲文本行,並使用FlatMapValues運算符將其拆分爲單詞:
KStream<String, String> source = builder.stream("streams-plaintext-input");
KStream<String, String> words = source.flatMapValues(new ValueMapper<String, Iterable<String>>() {
@Override
public Iterable<String> apply(String value) {
return Arrays.asList(value.split("\\W+"));
}
});
運算符將源流作爲輸入,並通過按順序處理其源流中的每個記錄並將其值字符串分成單詞列表,並將每個單詞作爲新記錄生成到輸出,來生成名爲單詞的新流。 文字流。 這是一個無狀態運算符,不需要跟蹤任何以前收到的記錄或已處理的結果。 請注意,如果您使用的是JDK 8,則可以使用lambda表達式並將上述代碼簡化爲:
KStream<String, String> source = builder.stream("streams-plaintext-input");
KStream<String, String> words = source.flatMapValues(value -> Arrays.asList(value.split("\\W+")));
最後,我們可以將stream這個詞寫回到另一個Kafka topic,即streams-linesplit-output。 同樣,可以將以下兩個步驟串聯起來(假設使用了lambda表達式):
KStream<String, String> source = builder.stream("streams-plaintext-input");
source.flatMapValues(value -> Arrays.asList(value.split("\\W+")))
.to("streams-linesplit-output");
如果現在將這種擴展拓撲描述爲System.out.println(topology.describe()),則會得到以下內容:
Topologies:
Sub-topology: 0
Source: KSTREAM-SOURCE-0000000000 (topics: [streams-plaintext-input])
--> KSTREAM-FLATMAPVALUES-0000000002, KSTREAM-FLATMAPVALUES-0000000001
Processor: KSTREAM-FLATMAPVALUES-0000000002 (stores: [])
--> KSTREAM-SINK-0000000003
<-- KSTREAM-SOURCE-0000000000
Processor: KSTREAM-FLATMAPVALUES-0000000001 (stores: [])
--> none
<-- KSTREAM-SOURCE-0000000000
Sink: KSTREAM-SINK-0000000003 (topic: streams-linesplit-output)
<-- KSTREAM-FLATMAPVALUES-0000000002
如上所示,新的處理器節點KSTREAM-FLATMAPVALUES-0000000001被注入到原始源節點和宿節點之間的拓撲中。 它以源節點爲父節點,宿節點爲子節點。 換句話說,由源節點獲取的每個記錄將首先遍歷到要添加的新添加的KSTREAM-FLATMAPVALUES-0000000001節點,結果將生成一個或多個新記錄。 它們將繼續遍歷到接收器節點以寫回到Kafka。 請注意,此處理器節點是“無狀態”的,因爲它沒有與任何存儲()相關聯。
完整的代碼如下所示(假設使用了lambda表達式):
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) throws Exception {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-linesplit");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost: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.split("\\W+")))
.to("streams-linesplit-output");
final Topology topology = builder.build();
final KafkaStreams streams = new KafkaStreams(topology, props);
final CountDownLatch latch = new CountDownLatch(1);
// ... same as Pipe.java above
}
}
編寫wordcount 應用程序
現在,我們進一步採取措施,通過對從源文本流中拆分出的單詞進行計數來向topology 添加一些“狀態”計算。 按照類似的步驟,讓我們基於LineSplit.java類創建另一個程序。
public class WordCount {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-wordcount");
// ...
}
}
爲了對單詞進行計數,我們可以首先修改flatMapValues運算符以將輸入的字符全部視爲小寫(假設使用了lambda表達式):
source.flatMapValues(new ValueMapper<String, Iterable<String>>() {
@Override
public Iterable<String> apply(String value) {
return Arrays.asList(value.toLowerCase(Locale.getDefault()).split("\\W+"));
}
});
爲了進行計數彙總,我們必須首先指定我們要使用groupBy運算符將流鍵入值字符串。 此運算符生成一個新的分組流,然後可以由count運算符聚合,該操作會在每個分組鍵上生成一個運行計數:
KTable<String, Long> counts =
source.flatMapValues(new ValueMapper<String, Iterable<String>>() {
@Override
public Iterable<String> apply(String value) {
return Arrays.asList(value.toLowerCase(Locale.getDefault()).split("\\W+"));
}
})
.groupBy(new KeyValueMapper<String, String, String>() {
@Override
public String apply(String key, String value) {
return value;
}
})
// Materialize the result into a KeyValueStore named "counts-store".
// The Materialized store is always of type <Bytes, byte[]> as this is the format of the inner most store.
.count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>> as("counts-store"));
請注意,count運算符具有一個Materialized參數,該參數指定運行計數應存儲在名爲counts-store的狀態存儲中。 可以實時查詢此Counts存儲。
我們還可以將計數KTable的changelog流寫回到另一個Kafka topic,即streams-wordcount-output。 由於結果是更改日誌流,因此應在啓用日誌壓縮的情況下配置寫出到topic :streams-wordcount-output。 請注意,這次值類型不再是String而是Long,因此默認的序列化類不再適用於將其寫入Kafka的情況。 我們需要爲Long類型提供重寫的序列化方法,否則將拋出運行時異常:
counts.toStream().to("streams-wordcount-output", Produced.with(Serdes.String(), Serdes.Long()));
請注意,爲了從主題streams-wordcount-output讀取changelog 流,需要將反序列化值設置爲org.apache.kafka.common.serialization.LongDeserializer。使用JDK 8中的lambda表達式,則上述代碼可以簡化爲:
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()));
使用System.out.println(topology.describe())來打印打錢topology的描述,可以得到下面這樣的輸出:
Topologies:
Sub-topology: 0
Source: KSTREAM-SOURCE-0000000000 (topics: [TextLinesTopic])
--> KSTREAM-FLATMAPVALUES-0000000001
Processor: KSTREAM-FLATMAPVALUES-0000000001 (stores: [])
--> KSTREAM-KEY-SELECT-0000000002
<-- KSTREAM-SOURCE-0000000000
Processor: KSTREAM-KEY-SELECT-0000000002 (stores: [])
--> KSTREAM-FILTER-0000000005
<-- KSTREAM-FLATMAPVALUES-0000000001
Processor: KSTREAM-FILTER-0000000005 (stores: [])
--> KSTREAM-SINK-0000000004
<-- KSTREAM-KEY-SELECT-0000000002
Sink: KSTREAM-SINK-0000000004 (topic: counts-store-repartition)
<-- KSTREAM-FILTER-0000000005
Sub-topology: 1
Source: KSTREAM-SOURCE-0000000006 (topics: [counts-store-repartition])
--> KSTREAM-AGGREGATE-0000000003
Processor: KSTREAM-AGGREGATE-0000000003 (stores: [counts-store])
--> KTABLE-TOSTREAM-0000000007
<-- KSTREAM-SOURCE-0000000006
Processor: KTABLE-TOSTREAM-0000000007 (stores: [])
--> KSTREAM-SINK-0000000008
<-- KSTREAM-AGGREGATE-0000000003
Sink: KSTREAM-SINK-0000000008 (topic: WordsWithCountsTopic)
<-- KTABLE-TOSTREAM-0000000007
正如我們在上面看到的那樣,topology 現在包含兩個互相無連接的的sub-topologies。第一個sub-topology的宿節點KSTREAM-SINK-0000000004將寫入分區topic Counts-repartition,第二個sub-topology的源節點KSTREAM-SOURCE-0000000006將讀取該分區。此外,在第一個sub-topology內部,將無狀態的KSTREAM-FILTER-0000000005節點注入到分組的KSTREAM-KEY-SELECT-0000000002節點與接收器節點之間,以過濾掉聚合鍵爲空的任何中間記錄。
在第二個子拓撲中,聚合節點KSTREAM-AGGREGATE-0000000003與名爲Counts的狀態存儲關聯(名稱由用戶在count運算符中指定)。從上游節點接收到每個記錄後,聚合處理器將首先查詢其關聯的Counts存儲,以獲取該鍵的當前計數,再增加一個,然後將新計數寫回到存儲中。密鑰的每個更新計數也將通過管道傳遞到KTABLE-TOSTREAM-0000000007節點,該節點將此更新流解釋爲記錄流,然後再進一步傳遞給接收器節點KSTREAM-SINK-0000000008以寫回Kafka。
完整的代碼如下所示(假設使用了lambda表達式):
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.KTable;
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.Properties;
import java.util.concurrent.CountDownLatch;
public class WordCount {
public static void main(final String[] args) throws Exception {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-application");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092");
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> textLines = builder.stream("TextLinesTopic");
KTable<String, Long> wordCounts = textLines
.flatMapValues(textLine -> Arrays.asList(textLine.toLowerCase().split("\\W+")))
.groupBy((key, word) -> word)
.count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("counts-store"));
wordCounts.toStream().to("WordsWithCountsTopic", Produced.with(Serdes.String(), Serdes.Long()));
final Topology topology = builder.build();
System.out.println(topology.describe());
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);
}
}