一、書寫背景:
最近陸續(xù)收到不少朋友反饋,他們在計(jì)劃要在springboot項(xiàng)目引入kafka中間件。在網(wǎng)上找過很多資料,但都比較零散,按照其說明進(jìn)行操作后讓項(xiàng)目正常跑起來仍是比較坎坷,聽說我對kafka比較了解,希望給予一起分享。剛好最近因?yàn)橐咔椋瑢?shí)際相對于平常稍寬松,也想借此寫點(diǎn)東西,一來作為自己的總結(jié),二來可以給予有需要的朋友一些引導(dǎo),針對此文期望對各位遇到問題的朋友有一定的幫助。
二、前期準(zhǔn)備:
1. 安裝JDK,具體版本可以根據(jù)項(xiàng)目實(shí)際情況選擇,目前使用最多的為jdk8
2.安裝Zookeeper,具體版本可以根據(jù)項(xiàng)目實(shí)際情況選擇,本項(xiàng)目使用的是3.5.8
3.安裝Kafka?,具體版本可以根據(jù)項(xiàng)目實(shí)際情況選擇,本項(xiàng)目使用的是3.5.1
4.安裝Kafka Manage? (非必要:安裝主要是了對kafka項(xiàng)目操作提供圖形化界面操作),具體版本可以根據(jù)項(xiàng)目實(shí)際情況選擇,本項(xiàng)目使用的是1.3.3.7
三、具體實(shí)現(xiàn):
1.pom.xml加依賴:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!--springboot web依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--kafka依賴 -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
</dependencies>
2.application.yml做配置
spring:
? kafka:
? ? bootstrap-servers: 127.0.0.1:9092
? ? producer: #生產(chǎn)者
? ? ? # 發(fā)生錯(cuò)誤后,消息重發(fā)的次數(shù)。重試1次,此值需要結(jié)合業(yè)務(wù)場景,重試與否各有千秋(重試,好處:盡可能的確保生產(chǎn)者寫入block成功;壞處:有可能時(shí)帶有順序?qū)懭氲臄?shù)據(jù)打亂順序
? ? ? #比如:依次寫入數(shù)據(jù) 1/2/3,但寫入1時(shí)因網(wǎng)絡(luò)異常,進(jìn)行了重寫,結(jié)果落到block的數(shù)據(jù)成了2/3/1)
? ? ? retries: 1
? ? ? #當(dāng)有多個(gè)消息需要被發(fā)送到同一個(gè)分區(qū)時(shí),生產(chǎn)者會把它們放在同一個(gè)批次里。該參數(shù)指定了一個(gè)批次可以使用的內(nèi)存大小,按照字節(jié)數(shù)計(jì)算。模式時(shí)16k
? ? ? batch-size: 16384 #16k
? ? ? # 設(shè)置生產(chǎn)者內(nèi)存緩沖區(qū)的大小
? ? ? buffer-memory: 33554432
? ? ? acks: 1
? ? ? # 鍵的序列化方式
? ? ? key-serializer: org.apache.kafka.common.serialization.StringSerializer
? ? ? # 值的序列化方式
? ? ? value-serializer: org.apache.kafka.common.serialization.StringSerializer
? ? consumer:
? ? ? #group-id: default-group
? ? ? # 自動(dòng)提交的時(shí)間間隔 在spring boot 2.X 版本中這里采用的是值的類型為Duration 需要符合特定的格式,如1S,1M,2H,5D.此屬性只有在enable-auto-commit:true時(shí)生效
? ? ? auto-commit-interval: 1S
? ? ? enable-auto-commit: false
? ? ? # 該屬性指定了消費(fèi)者在讀取一個(gè)沒有偏移量的分區(qū)或者偏移量無效的情況下該作何處理:
? ? ? # latest(默認(rèn)值)在偏移量無效的情況下,消費(fèi)者將從最新的記錄開始讀取數(shù)據(jù)(在消費(fèi)者啟動(dòng)之后生成的記錄)
? ? ? # earliest :在偏移量無效的情況下,消費(fèi)者將從起始位置讀取分區(qū)的記錄
? ? ? auto-offset-reset: earliest
? ? ? # 鍵的反序列化方式
? ? ? key-deserializer: org.apache.kafka.common.serialization.StringSerializer
? ? ? # 值的反序列化方式
? ? ? value-deserializer: org.apache.kafka.common.serialization.StringSerializer
? ? listener:
? ? ? # 當(dāng)每一條記錄被消費(fèi)者監(jiān)聽器(ListenerConsumer)處理之后提交
? ? ? # RECORD
? ? ? # 當(dāng)每一批poll()的數(shù)據(jù)被消費(fèi)者監(jiān)聽器(ListenerConsumer)處理之后提交
? ? ? # BATCH
? ? ? # 當(dāng)每一批poll()的數(shù)據(jù)被消費(fèi)者監(jiān)聽器(ListenerConsumer)處理之后,距離上次提交時(shí)間大于TIME時(shí)提交
? ? ? # TIME
? ? ? # 當(dāng)每一批poll()的數(shù)據(jù)被消費(fèi)者監(jiān)聽器(ListenerConsumer)處理之后,被處理record數(shù)量大于等于COUNT時(shí)提交
? ? ? # COUNT
? ? ? # TIME | COUNT 有一個(gè)條件滿足時(shí)提交
? ? ? # COUNT_TIME
? ? ? # 當(dāng)每一批poll()的數(shù)據(jù)被消費(fèi)者監(jiān)聽器(ListenerConsumer)處理之后, 手動(dòng)調(diào)用Acknowledgment.acknowledge()后提交
? ? ? # MANUAL
? ? ? # 手動(dòng)調(diào)用Acknowledgment.acknowledge()后立即提交,一般使用這種
? ? ? # MANUAL_IMMEDIATE
? ? ? ack-mode: manual_immediate
? ? ? # 在偵聽器容器中運(yùn)行的線程數(shù)
? ? ? concurrency: 5
3.創(chuàng)建topic:
3.1 通過命令創(chuàng)建,如下:
bin/kafka-topics.sh --create --zookeeper 127.0.0.1:2181 --replication-factor 1 --partitions 1 --topic test
?-- 127.0.0.1:2181? zookeeper?服務(wù)器地址
--??replication-factor?partitions 副本數(shù)量
--partitions?partition數(shù)量
3.2 通過Kafka Manager創(chuàng)建(推薦使用),操作如下:
3.2.1 新建Cluster
點(diǎn)擊【Cluster】>【Add Cluster】打開如下添加集群的配置界面:
輸入集群的名字(如Kafka-Cluster-1)和 Zookeeper 服務(wù)器地址(如localhost:2181),選擇最接近的Kafka版本(如0.10.1.0)
3.2.2 新建主題
4.編代碼:
4.1 定義消息生產(chǎn)者 API
package com.charlie.cloudconsumer.service.impl.kafka;
import com.charlie.cloudconsumer.common.utils.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import java.util.UUID;
/**
* @Author: charlie
* @CreateTime: 2022/4/9
* @Description : kafka消息生產(chǎn)端,為實(shí)踐業(yè)務(wù)提供向kafka block發(fā)現(xiàn)消息的API
*/
@Component
@Slf4j
public class QueueProducer {
@Autowired
? ? private KafkaTemplatekafkaTemplate;
public void sendQueue(String topic,Object msgContent) {
String obj2String =JSON.toJSONString(msgContent);
log.info("kafka service 準(zhǔn)備發(fā)送消息為:{}",obj2String);
//發(fā)送消息
? ? ? ? ListenableFuture>future =kafkaTemplate.send(topic,UUID.randomUUID().toString(),obj2String);
future.addCallback(new ListenableFutureCallback>() {
//消息發(fā)送成功
? ? ? ? ? ? @Override
? ? ? ? ? ? public void onSuccess(SendResult stringObjectSendResult) {
log.info("[kafka service-生產(chǎn)成功]topic:{},結(jié)果{}",topic, stringObjectSendResult.toString());
}
//消息發(fā)送失敗
? ? ? ? ? ? @Override
? ? ? ? ? ? public void onFailure(Throwable throwable) {
//發(fā)送失敗的處理,本處只是記錄了錯(cuò)誤日志,可結(jié)合實(shí)際業(yè)務(wù)做處理
? ? ? ? ? ? ? ? log.info("[kafka service-生產(chǎn)失敗]topic:{},失敗原因{}",topic, throwable.getMessage());
}
});
}
}
4.2 定義消息處理業(yè)務(wù)類
package com.charlie.cloudconsumer.service.impl.kafka;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Component;
/**
* @Author: charlie
* @CreateTime: 2022/4/9
* @Description : 消費(fèi)端實(shí)際的業(yè)務(wù)處理對象
*/
@Component //添加此注解的原因是因?yàn)橄M(fèi)端在項(xiàng)目啟動(dòng)時(shí)就會初始化,消費(fèi)端需要用到此類,故也讓此類在項(xiàng)目啟動(dòng)時(shí)進(jìn)行注冊
public class QueueDataProcess {
public boolean doExec(Object obj) {
// todu 具體的業(yè)務(wù)邏輯
? ? ? ? if (ObjectUtils.isNotEmpty(obj)) {
return true;
}else {
return false;
}
}
}
4.3 定義消費(fèi)者
package com.charlie.cloudconsumer.service.impl.kafka;
import com.charlie.cloudconsumer.common.utils.JSON;
import com.charlie.cloudconsumer.model.Order;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.kafka.support.KafkaHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* @Author: charlie
* @CreateTime: 2022/4/9
* @Description : kafka消息消費(fèi)端,負(fù)責(zé)消費(fèi)特定topic消息
*/
@Component
@Slf4j
@SuppressWarnings("all")
public class QueueConsumer {
@Autowired
? ? private QueueDataProcess queueDataProcess;
/**
*
*/
? ? @KafkaListener(topics ="test", groupId ="consumer")
public void doConsumer(ConsumerRecord record,Acknowledgment ack,@Header(KafkaHeaders.RECEIVED_TOPIC)String topic) {
Optional message =Optional.ofNullable(record.value());
if (message.isPresent()) {
try {
Object msg =message.get();
log.info("[kafka-消費(fèi)] doConsumer 消費(fèi)了: Topic:" + topic +",Message:" +msg);
boolean res =queueDataProcess.doExec(JSON.parseObject(msg.toString(),Order.class));
if (res) {
ack.acknowledge();
}
}catch (Exception ex) {
log.error("[kafka-消費(fèi)異常] doConsumer Error {} ",ExceptionUtils.getFullStackTrace(ex));
}
}
}
}
4.4 定義控制器
package com.charlie.cloudconsumer.controller;
import com.alibaba.fastjson.JSON;
import com.charlie.cloudconsumer.common.utils.AjaxResult;
import com.charlie.cloudconsumer.common.utils.BuildResponseUtils;
import com.charlie.cloudconsumer.model.Order;
import com.charlie.cloudconsumer.service.impl.kafka.QueueProducer;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: charlie
* @CreateTime: 2022/4/9
* @Description : kafka消息發(fā)送控制器,負(fù)責(zé)接受用戶的發(fā)送的隊(duì)列消息
*/
@RestController
@RequestMapping(value ="/kafka",produces =MediaType.APPLICATION_JSON_VALUE)
public class KafkaController {
? ? @Autowired
? ? private QueueProducer queueProducer;
? ? @RequestMapping(value = "/send",method = RequestMethod.POST)
? ? public? AjaxResult<?> sendMsg(@RequestBody Order order) {
? ? ? ? AjaxResult<?> ajaxResult= null;
? ? ? ? if (ObjectUtils.isNotEmpty(order)) {
? ? ? ? ? queueProducer.sendQueue("test",order);
? ? ? ? ? ? ajaxResult = BuildResponseUtils.success(0,"發(fā)送消息:"+ JSON.toJSONString(order) + "成功!");
? ? ? ? } else {
? ? ? ? ? ? ajaxResult = BuildResponseUtils.success(1,"發(fā)送消息:"+ JSON.toJSONString(order) + "失敗!");
? ? ? ? }
? ? ? ? return ajaxResult;
? ? }
}