前言
在大多數場景中,我們經常使用kafka來做釋出-訂閱,在釋出-訂閱模型中,訊息一旦傳送就不再追蹤後續處理,但在某些業務場景下,我們希望在傳送訊息後等待一個響應,然後根據這個響應來做我們後續的操作。在這種請求-響應模式,我們就可以利用spring kafka的ReplyingKafkaTemplate來實現
ReplyingKafkaTemplate
簡介
ReplyingKafkaTemplate 是 Spring Kafka 中的一個高階特性,專門用於處理 Kafka 中的請求/響應模式。它允許你傳送一個訊息到 Kafka,並等待一個響應
### 使用場景
微服務間非同步請求-響應: 當一個微服務需要從另一個微服務獲取資料或執行操作,並希望在操作完成後得到通知時,可以使用 ReplyingKafkaTemplate。
- 狀態查詢:<em> 如果一個服務需要定期或按需查詢另一個服務的狀態,但又不希望阻塞主執行緒等待響應,可以使用此模板。
- 非同步任務確認: 當一個服務發起一個非同步任務(如檔案上傳、計算任務等),並需要知道任務何時完成時,可以使用 ReplyingKafkaTemplate 來接收完成通知
如何使用
1、在專案中引入spring-kafka gav
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
2、 配置replyingKafkaTemplate bean
注: ReplyingKafkaTemplate需依賴ProducerFactory和KafkaMessageListenerContainer
配置示例如下
/**
* 建立一個repliesContainer
* @param containerFactory
* @return
*/
@Bean
public ConcurrentMessageListenerContainer<String, String> repliesContainer(
ConcurrentKafkaListenerContainerFactory<String, String> containerFactory) {
// 和RecordHeader中的topic對應起來
ConcurrentMessageListenerContainer<String, String> repliesContainer =
containerFactory.createContainer(KafkaConstant.REPLY_TOPIC);
repliesContainer.getContainerProperties().setGroupId("repliesGroup");
repliesContainer.setAutoStartup(false);
return repliesContainer;
}
/**
* 建立一個replyingTemplate
* @param pf
* @param repliesContainer
* @return
*/
@Bean
public ReplyingKafkaTemplate<String, String, String> replyingTemplate(
ProducerFactory<String, String> pf,
ConcurrentMessageListenerContainer<String, String> repliesContainer) {
ReplyingKafkaTemplate<String, String, String> replyingKafkaTemplate = new ReplyingKafkaTemplate<>(pf, repliesContainer);
// 設定響應超時為10秒,預設5秒
replyingKafkaTemplate.setReplyTimeout(10000);
return replyingKafkaTemplate;
}
3、producer傳送請求並等待響應
@SneakyThrows
@Override
public String sendAndReceive(String topic, ParamRequest request) {
// 建立ProducerRecord類,用來傳送訊息
ProducerRecord<String,String> producerRecord = new ProducerRecord<>(topic, JSONUtil.toJsonStr(request));
// 新增KafkaHeaders.REPLY_TOPIC到record的headers引數中,這個引數配置我們想要轉發到哪個Topic中
producerRecord.headers().add(new RecordHeader(KafkaHeaders.REPLY_TOPIC, KafkaConstant.REPLY_TOPIC.getBytes()));
// sendAndReceive方法返回一個Future類RequestReplyFuture,
// 這裡類裡面包含了獲取傳送結果的Future類和獲取返回結果的Future類。
// 使用replyingKafkaTemplate傳送及返回都是非同步操作
RequestReplyFuture<String, String, String> replyFuture = replyingKafkaTemplate.sendAndReceive(producerRecord);
// 獲取傳送結果
SendResult<String, String> sendResult = replyFuture.getSendFuture().get();
log.info("send message success,topic:{},message:{},sendResult:{}",topic,JSONUtil.toJsonStr(request),sendResult.getRecordMetadata());
// 獲取響應結果
ConsumerRecord<String, String> consumerRecord = replyFuture.get();
String result = consumerRecord.value();
log.info("result: {}",result);
return result;
}
注: 方法裡都寫了相應註釋,就不再論述了
4、consumer進行監聽,並將返回結果透過@SendTo轉發回去
在官網貼了這麼一段話
他的大意是為了支援@SendTo,偵聽器容器工廠必須提供一個KafkaTemplate(在其replyTemplate屬性中),用於傳送回覆。因此我們做如下配置
@Bean
@ConditionalOnMissingBean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory(KafkaTemplate kafkaTemplate, ConsumerFactory consumerFactory) {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
factory.setReplyTemplate(kafkaTemplate);
return factory;
}
上述配置好後,就配置下監聽器
@KafkaListener(topics = TOPIC, groupId = "${lybgeek.consumer.group-id-prefix:lybgeek}-group-id", containerFactory = "kafkaListenerContainerFactory")
/**
* @SendTo 是一個Spring註解,常用於 Kafka 消費者方法之上,指示訊息處理完成後應當將響應傳送到哪個 Kafka 主題。
* 使用場景:當你的應用作為服務端,需要對某個主題上的訊息做出響應時,可以在處理該訊息的方法上使用此註解來指定響應訊息的目標主題。
* 特點:簡化了響應訊息的路由配置,使得開發者無需顯式地編寫訊息傳送邏輯,只需關注業務處理邏輯。
* 配合 ReplyingKafkaTemplate:在請求/響應模式中,@SendTo 指定的響應主題與 ReplyingKafkaTemplate 傳送請求時設定的期望響應主題相匹配,從而使得請求方能夠正確地接收響應訊息。
*/
//@SendTo("hello-test")
@SendTo
public String listen(String data, Acknowledgment ack) {
log.info("receive data:{}",data);
if(JSONUtil.isJson(data)){
Object result = execute(JSONUtil.toBean(data, ParamRequest.class),ack);
if(result != null){
ack.acknowledge();
return JSONUtil.toJsonStr(result);
}
;
}
return null;
}
@SendTo的用途看我程式碼註釋,具體用法可以檢視官網
https://docs.spring.io/spring-kafka/reference/kafka/receiving-messages/annotation-send-to.html
進行了解
5、寫個測試控制器
這個控制器的作用就是客戶端發起http請求後,將請求引數送往kafka,kafka的消費方接收到http請求後,進行業務處理,並將業務結果透過kafka轉發回去
@PostMapping(value = "/**", consumes = {MediaType.APPLICATION_JSON_VALUE})
public Mono<ResponseEntity<byte[]>> forward(ProxyExchange<byte[]> proxy, Object params, HttpMethodEnum httpMethodEnum){
try {
String path = proxy.path().replace("/kafka", "").trim();
ParamRequest paramRequest = buildParamRequest(path,params,httpMethodEnum);
String topicCode = StringUtils.hasText(topicThreadLocal.get()) ? topicThreadLocal.get() : DEFAULT_TOPIC;
String topic = TOPIC.replace(DEFAULT_TOPIC_PATTERN,topicCode);
log.info(">>>>>>>>>>>>>> topic:{},httpMethod:{}, path:{},params:{}",topic,httpMethodEnum,path,params);
Object result = kafkaService.sendAndReceive(topic,paramRequest);
if(result != null){
return Mono.just(ResponseEntity.ok(result.toString().getBytes()));
}
} catch (Exception e) {
log.error(">>>>>>>>>>>>>> httpMethod:{},forward --> e:{}",httpMethodEnum.toString(),e.getMessage());
} finally {
topicThreadLocal.remove();
}
return Mono.just(ResponseEntity.ok(new byte[0]));
}
核心就是這句程式碼
kafkaService.sendAndReceive(topic,paramRequest);
詳細示例,可以檢視文末的demo連結
使用ReplyingKafkaTemplate遇到的問題
No pending reply
這個問題是因為我複製了消費端配置檔案,它配置了手動提交,而 ReplyingKafkaTemplate 是傳送請求的一方,通常不需要特別的手動確認機制,因為 ReplyingKafkaTemplate 會等待響應或超時,因此改成自動確認即可
具體配置如下
spring:
kafka:
consumer:
enable-auto-commit: ${KAFKA_CONSUMER_ENABLE_AUTO_COMMIT:true}
或者直接將消費端配置去掉也可以
總結
本文介紹透過ReplyingKafkaTemplate來實現請求-響應模式,在實際使用中,考慮到網路延遲和處理時間,呼叫ReplyingKafkaTemplate#sendAndReceive 方法可能會阻塞一段時間,因此在高負載環境下可能需要增加超時設定或使用回撥機制
demo連結
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-kafka-forward