聊聊如何利用kafka實現請求-響應模式

linyb极客之路發表於2024-12-03

前言

在大多數場景中,我們經常使用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

相關文章