基礎環境搭建(參照官網)好了之後直接搞,kafka的配置文件server.properties詳細說明戳我。下面的Demo都是基於win10搭建出來的基礎環境。
1. 基本概念
Kafka是一個分佈式流處理平臺,注意它是使用流來處理數據的(核心特徵之一),最終是爲了能夠進行實時的流處理。它是一個集羣,通過 topic 對其中存儲的數據進行分類,每條記錄包含一個 key、一個value、一個時間戳。官網上總結Kafka有4個核心API:
- The Producer API:允許一個應用程序發佈一串流式的數據到一個或者多個Kafka topic。
- The Consumer API:允許一個應用程序訂閱一個或多個 topic ,並且對發佈給他們的流式數據進行處理。
- The Streams API:允許一個應用程序作爲一個流處理器,消費一個或者多個topic產生的輸入流,然後生產一個輸出流到一個或多個topic中去,在輸入輸出流中進行有效的轉換。
- The Connector API 允許構建並運行可重用的生產者或者消費者,將Kafka topics連接到已存在的應用程序或者數據系統。比如,連接到一個關係型數據庫,捕捉表(table)的所有變更內容。
kafka中topic是一個非常重要的概念,本身的目的也是提供遺傳流式的記錄(即topic)。一些重要的基本概念如下:
- Broker:可以理解爲各個kafka的節點標識,通常用
broker.id
來標識; - Topic:數據主題,是數據記錄發佈的地方,可以用來區分業務系統,通常一個Topic可以擁有多個消費者來訂閱;
- Producer:生產者,將數據發佈到指定的topic的某個 partition 中,這個行爲可以使用循環或某些語義函數實現負載均衡;
- Consumer:消費者,由消費者組名稱進行標識,發佈到topic中的記錄將會分配給訂閱該topic的消費者;
- Consumer group:消費者組,由多個消費者實例組成(可以分佈在多個進程或機器上),對應邏輯上的一個訂閱者;
- Partition:kafka 對每個 topic 都會維護一個分區,每個分區都是有序的記錄集(順序不可變),且日誌會不斷追加到結構化的 commit log 文件中,分區中每個記錄都會分配一個id(即 offset)來標識上述的順序,topic的分區可以是若干個副本,當節點出現問題時,能自動進行故障轉移,保證數據的可用性;
- Replication:這裏的副本即指partition的副本,創建副本的單位是topic的partition,通常每個分區都有一個leader和0或多個followers,副本數目=leader數+follower數,所有的follower節點都會同步leader節點的日誌(故障轉移的一種機制)。
日誌中的partition有一下2個作用:
- 當日志大小超過了單臺服務器的限制,允許日誌進行擴展。每個單獨的分區都必須受限於主機的文件限制,不過一個主題可能有多個分區,因此可以處理無限量的數據;
- 可以作爲並行的單元集;
kafka 有一些質量保證:生產者發送到某個topic的消息會按照發送的順序處理,即先發的消息記錄的偏移(offset)必定比後發的消息的偏移小,且在日誌中較早出現;消費者實例按照日誌中的順序查看;如果一個主題有N個副本,那kafka最多容忍N-1個服務故障,保證數據不會丟失。在kafka中,各個節點是否“存活”,主要取決於下面2個標準:
- 節點必須可以維護和 ZooKeeper 的連接,Zookeeper 通過心跳機制檢查每個節點的連接;
- 如果節點是個 follower ,它必須能及時的同步 leader 的寫操作,並且延時不能太久;
只有滿足上述2個標準的節點纔是kafka認可的in sync
狀態。Leader會追蹤所有 “in sync” 的節點。如果有節點掛掉了, 或是寫超時, 或是心跳超時, leader 就會把它從同步副本列表中移除。 同步超時和寫超時的時間由replica.lag.time.max.ms
配置確定
2. 基本命令
Talk is cheap. Show me the code,實操纔是最好的熟悉新事物的方式,下面是一些簡單的操作:
# 1. --create 指定當前對於topic的操作行爲
# --topic 指定名爲 test 的 topic
# --zookeeper 指定 zookeeper 地址爲 localhost:2181
# --replication-factor 指定 topic 的副本數量爲3
# --partitions 指定分區 必選
.\kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
# 2. 查看topic列表
.\kafka-topics.bat --list --zookeeper localhost:2181
# 3. 查看某個topic(這裏是my-replicated-topic)的詳細信息
.\kafka-topics.bat --describe --zookeeper localhost:2181 --topic my-replicated-topic
# 輸出如下, 其中第一行標識所有分區的摘要,後續的第二行第三行..標識每個分區的信息,my-replicated-topic 只有一個分區,故只有一行
Topic:my-replicated-topic PartitionCount:1 ReplicationFactor:3 Configs:
Topic: my-replicated-topic Partition: 0 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2
# 4. 修改 topic 的 partitions 爲2
.\kafka-topics.bat --zookeeper localhost:2181 --alter --topic test --partitions 2
# 再次查看 test 的詳情:.\kafka-topics.bat --describe --zookeeper localhost:2181 --topic test 輸出如下
Topic:test PartitionCount:2 ReplicationFactor:1 Configs:
Topic: test Partition: 0 Leader: 0 Replicas: 0 Isr: 0
Topic: test Partition: 1 Leader: 1 Replicas: 1 Isr: 1
# 4.1 增加配置項:對 topic test 添加某個配置項 x=y (key-value的形式)
.\kafka-configs.bat --zookeeper localhost:2181 --entity-type topics --entity-name test --alter --add-config x=y
# 4.2 刪除配置項
.\kafka-configs.bat --zookeeper localhost:2181 --entity-type topics --entity-name test --alter --delete-config x=y
# 5. 向集羣發送消息,運行生產者,選擇某個主題,這裏選擇 test 的topic
.\kafka-console-producer.bat --broker-list localhost:9092 --topic test
# 6. 從集羣消費消息,運行消費者,選擇和生產者一致的topic進行連接,當然可以是集羣中的任意kafka節點,比如我這裏是 9092~9094 3個節點
.\kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test --from-beginning
# 7. 刪除topic,如果配置文件中沒有配置 delete.topic.enable=true 刪除操作時並不會真正刪除,只是打上一個標記
.\kafka-topics.bat --delete --zookeeper localhost:2181 --topic my-replicated-topic
上述 --describe
行爲輸出結果詳細參數說明如下:
- “leader”是負責給定分區所有讀寫操作的節點。每個節點都是隨機選擇的部分分區的領導者。
- “replicas”是複製分區日誌的節點列表,不管這些節點是leader還是僅僅活着。
- “isr”是一組“同步”replicas,是replicas列表的子集,它活着並被指到leader。
通過在生產者balabala一些,他就會將你說的這些廢話(默認一行廢話爲一個message)發送到kafka的集羣,集羣內的所有消費者都會接受你的廢話,如下:
上述的基本命令都是topic級別的,很多參數都可以通過 --config
的方式指定,比如上述的--create
、--zookeeper
、--replication-factor
,當然還有很多參數可以配置,比如:--config max.message.bytes=64000
、--config flush.messages=1
指定最大消息的大小和刷新頻率。topic的創建行爲可以像上述那樣手動創建,也可以在數據首次發佈到某個不存在的topic時自動創建,對於一些參數詳細說明如下:
--replication-factor
:控制有多少服務器將複製每個寫入的消息。如果設置了3個複製因子,那麼只能最多2個相關的服務器能出問題,否則將無法訪問數據(N-1)。建議您使用2或3個複製因子,以便在不中斷數據消費的情況下透明的調整集羣;--partitions
:控制 topic 將被分片到多少個日誌裏。partitions 會產生幾個影響-首先,每個分區只屬於一個臺服務器,所以如果有20個分區,那麼全部數據(包含讀寫負載)將由不超過20個服務器(不包含副本)處理,最後 partitions 還會影響 consumer 的最大並行度。
每個分區日誌都放在自己的Kafka日誌目錄下的文件夾中。這些文件夾的名稱的形式爲topic名-分區ID
,注意topic的名稱不能超過249個字符(爲了留下了足夠的空間以顯示短劃線和可能的5位長的分區ID)。
3. SpringBoot集成kafka
基於上述進行簡單的集成,基本環境如下:
- SpringBoot:2.2.1.RELEASE;
- spring-kafka:2.3.4.RELEASE;
- kafka:本地 win10 搭建的 kafka-2.3.1 3節點集羣;
- zookeeper:本地 win 10 搭建的 zookeeper-3.5.6;
pom 文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.glodon</groupId>
<artifactId>spring_kafka</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring_kafka</name>
<packaging>jar</packaging>
<description>Spring project for Kafka</description>
<properties>
<java.version>1.8</java.version>
<spring-kafka.version>2.3.4.RELEASE</spring-kafka.version>
</properties>
<dependencies>
<!--spring-boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--fastJson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<!-- kafka-->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>${spring-kafka.version}</version>
</dependency>
</dependencies>
</project>
相關參數配置:
# kafka
spring.kafka.bootstrap-servers=127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094
spring.kafka.listener.concurrency=3
# 生產者相關配置
spring.kafka.producer.retries=1
spring.kafka.producer.batch-size=16384
spring.kafka.producer.buffer-memory=33554432
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
# 消費者相關配置
spring.kafka.consumer.group-id=consumer-default
spring.kafka.consumer.auto-commit-interval=100
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
生產者和消費者實現類:
/**
* @author liuwg-a
* @date 2019/1/4 15:49
* @description 實例化配置,主要用於創建2個測試的topic
*/
@Configuration
public class Config {
/**
* 創建一個測試的 topic
*
* @return NewTopic
*/
@Bean("noBlockingSpringKafkaTopic")
public NewTopic createNoBlockingSpringKafkaTopic() {
return TopicBuilder.name("spring-kafka-no-blocking-test").partitions(10).replicas(3).config(TopicConfig.COMPRESSION_TYPE_CONFIG,
"zstd").build();
}
@Bean("blockingSpringKafkaTopic")
public NewTopic createBlockingSpringKafkaTopic() {
return TopicBuilder.name("spring-kafka-blocking-test").partitions(10).replicas(3).config(TopicConfig.COMPRESSION_TYPE_CONFIG,
"zstd").build();
}
}
/**
* @author liuwg-a
* @date 2019/12/10 19:01
* @description kafka 消息類
*/
public class Message {
private Long id;
private String msg;
private LocalDateTime time;
public Message() {
}
public Message(Long id, String msg, LocalDateTime time) {
this.id = id;
this.msg = msg;
this.time = time;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public LocalDateTime getTime() {
return time;
}
public void setTime(LocalDateTime time) {
this.time = time;
}
}
/**
* 生產者
* @author liuwg-a
* @date 2019/12/10 19:04
* @description
*/
@Service("producer")
public class KafkaProducer {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Autowired
private NewTopic noBlockingSpringKafkaTopic;
@Autowired
private NewTopic blockingSpringKafkaTopic;
private static final Logger logger = LoggerFactory.getLogger(KafkaProducer.class);
/**
* 1. 異步非阻塞發送消息
*
* @param message message
*/
public void noBlockingSend(Message message) {
if (message == null) {
return;
}
ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(noBlockingSpringKafkaTopic.name(),
JSON.toJSONString(message));
/*
* 消息發送成功進行回調
*/
future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
@Override
public void onFailure(Throwable throwable) {
// 處理失敗的邏輯
logger.error("kafka producer:send message failed");
}
@Override
public void onSuccess(SendResult<String, String> stringStringSendResult) {
// 處理成功的邏輯
logger.info("kafka producer: send message success");
}
});
}
/**
* 2. 阻塞式發送消息
*
* @param message message
*/
public void blockingSend(Message message) {
try {
kafkaTemplate.send(blockingSpringKafkaTopic.name(), JSON.toJSONString(message)).get(5, TimeUnit.SECONDS);
// 發送成功立即處理
logger.info("kafka producer: send message success");
} catch (Exception e) {
// 處理失敗的邏輯
logger.error("kafka producer:send message failed");
}
}
}
/**
* 消費者
* @author liuwg-a
* @date 2019/12/10 19:06
* @description
*/
@Service("consumer")
public class KafkaConsumer {
private static final String NO_BLOCKING_SPRING_KAFKA_TOPIC_STR = "spring-kafka-no-blocking-test";
private static final String BLOCKING_SPRING_KAFKA_TOPIC_STR = "spring-kafka-blocking-test";
private static final Logger logger = LoggerFactory.getLogger(KafkaConsumer.class);
/**
* 註解 @KafkaListener 會促使 Spring Kafka 自動創建一個消息容器
*/
@KafkaListener(topics = NO_BLOCKING_SPRING_KAFKA_TOPIC_STR, groupId = NO_BLOCKING_SPRING_KAFKA_TOPIC_STR)
public void receiveNoBlocking(ConsumerRecord<String, String> record) {
Optional<String> kafkaMessage = Optional.ofNullable(record.value());
if (kafkaMessage.isPresent()) {
Object message = kafkaMessage.get();
logger.info("kafka consumer: receive message -> record = {}", record);
logger.info("kafka consumer: receive message -> message = {}", message);
}
}
@KafkaListener(topics = BLOCKING_SPRING_KAFKA_TOPIC_STR, groupId = BLOCKING_SPRING_KAFKA_TOPIC_STR)
public void receiveBlocking(ConsumerRecord<String, String> record) {
Optional<String> kafkaMessage = Optional.ofNullable(record.value());
if (kafkaMessage.isPresent()) {
Object message = kafkaMessage.get();
logger.info("kafka consumer: receive message -> record = {}", record);
logger.info("kafka consumer: receive message -> message = {}", message);
}
}
}
測試類:
/**
* @author liuwg-a
* @date 2019/12/10 19:10
* @description
*/
@SpringBootTest
@ExtendWith(SpringExtension.class)
class KafkaProducerTest {
@Autowired
private KafkaProducer kafkaProducer;
@Test
void noBlockingSend() {
for (int i = 0; i < 3; i++) {
kafkaProducer.noBlockingSend(produceMessage());
}
}
@Test
void blockingSend() {
for (int i = 0; i < 3; i++) {
kafkaProducer.blockingSend(produceMessage());
}
}
/**
* 模擬生產消息
*/
private Message produceMessage() {
return new Message(new Random().nextLong(), UUID.randomUUID().toString(), LocalDateTime.now());
}
}
啓動測試即可,注意這裏如果不進行初始化創建的2個topic的java配置,則需要提前手動創建好測試的 topic,否則啓動會出現找不到該 topic 的異常。