使用Spring Request-Reply實現基於Kafka的同步請求響應

banq發表於2018-07-23
大家提到Kafka時第一印象就是它是一個快速的非同步訊息處理系統,不同於通常tomcat之類應用伺服器和前端之間的請求/響應方式請求,客戶端發出一個請求,必然會等到一個響應,這種方式對Kafka來說好像不適合,因為Kafka是一種事件驅動方式,透過事件才能啟用一個響應,但是,問題來了,很多人習慣請求響應模型以後很難接受這種事件響應模型,包括髮布訂閱模型。

當然,Kafka不是不能實現通常的請求響應模型,只要使用兩個Kafka主題,一個是負責請求的主題,另外一個是負責響應的主題,還必須在訊息的生產者記錄中構建相關ID,將與訊息的消費者記錄中的ID進行對應關聯起來,實際上就是將請求Id和響應Id進行關聯。

客戶端---->請求的主題 ----消費者處理請求並把結果傳送到---->響應主題--->客戶端
<p class="indent">


隨著Spring-Kafka最新版本推出(Spring replying kafka 模板),這種請求-響應模型實現就更加簡單了,不需要開發人員自己進行請求Id和響應Id的關聯,由Spring kafka模板實現。

下面這個案例例演示了Spring-Kafka是如何實現同步的請求響應模型的,原始碼見github

下圖是本案例的演示架構圖,這個案例是以同步行為返回兩個數字總和的結果。

客戶端-->請求-->RESTcontroll-->Spring-kafka模板-->Kafka請求主題-->Kafka監聽器 
               
客戶端<--響應<--RESTcontroll<--Spring-kafka模板<--Kafka響應主題<--Kafka監聽器
  
<p class="indent">


下面我們開始看看開發這個演示步驟:

設定Springboot啟動類
首先需要在pom.xml引入Spring kafka模板:

  <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>
<p class="indent">

程式碼如下:

@SpringBootApplication
public class RequestReplyKafkaApplication {

  public static void main(String[] args) {
    SpringApplication.run(RequestReplyKafkaApplication.class, args);
  }
}

<p class="indent">



設定Spring ReplyingKafkaTemplate
我們需要在Springboot配置類的KafkaConfig對Spring kafka模板進行配置:

@Configuration
public class KafkaConfig {
<p class="indent">


在這個配置類中,我們需要配置核心的ReplyingKafkaTemplate類,這個類繼承了 KafkaTemplate 提供請求/響應的的行為;還有一個生產者工廠(參見 ProducerFactory 下面的程式碼)和 KafkaMessageListenerContainer。這是最基本的設定,因為請求響應模型需要對應到訊息生產者和消費者的行為。

// 這是核心的ReplyingKafkaTemplate
@Bean
public ReplyingKafkaTemplate<String, Model, Model> replyKafkaTemplate(ProducerFactory<String, Model> pf, KafkaMessageListenerContainer<String, Model> container) {
  return new ReplyingKafkaTemplate<>(pf, container);
}

// 配件:監聽器容器Listener Container to be set up in ReplyingKafkaTemplate
@Bean
public KafkaMessageListenerContainer<String, Model> replyContainer(ConsumerFactory<String, Model> cf) {
  ContainerProperties containerProperties = new ContainerProperties(requestReplyTopic);
  return new KafkaMessageListenerContainer<>(cf, containerProperties);
}

// 配件:生產者工廠Default Producer Factory to be used in ReplyingKafkaTemplate
@Bean
public ProducerFactory<String,Model> producerFactory() {
  return new DefaultKafkaProducerFactory<>(producerConfigs());
}

// 配件:kafka生產者的Kafka配置Standard KafkaProducer settings - specifying brokerand serializer 
@Bean
public Map<String, Object> producerConfigs() {
  Map<String, Object> props = new HashMap<>();
  props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
            bootstrapServers);
  props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
            StringSerializer.class);
  props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
  return props;
}
<p class="indent">


設定spring-Kafka的監聽器
這與通常建立的Kafka消費者相同。唯一的變化是額外是在工廠中設定ReplyTemplate,這是必須的,因為消費者需要將計算結果放入到Kafka的響應主題。

//消費者工廠 Default Consumer Factory
@Bean
public ConsumerFactory<String, Model> consumerFactory() {
  return new DefaultKafkaConsumerFactory<>(consumerConfigs(),new StringDeserializer(),new JsonDeserializer<>(Model.class));
}

// 併發監聽器容器Concurrent Listner container factory
@Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, Model>> kafkaListenerContainerFactory() {
  ConcurrentKafkaListenerContainerFactory<String, Model> factory = new ConcurrentKafkaListenerContainerFactory<>();
  factory.setConsumerFactory(consumerFactory());
  // NOTE - set up of reply template 設定響應模板
  factory.setReplyTemplate(kafkaTemplate());
  return factory;
}

// Standard KafkaTemplate
@Bean
public KafkaTemplate<String, Model> kafkaTemplate() {
  return new KafkaTemplate<>(producerFactory());
}
<p class="indent">


編寫我們的kafka消費者
這是過去建立的Kafka消費者一樣。唯一的變化是附加了@SendTo註釋,此註釋用於在響應主題上返回業務結果。

@KafkaListener(topics = "${kafka.topic.request-topic}")
@SendTo
public Model listen(Model request) throws InterruptedException {
  int sum = request.getFirstNumber() + request.getSecondNumber();
  request.setAdditionalProperty("sum", sum);
  return request;
}
<p class="indent">

這個消費者用於業務計算,把客戶端透過請求傳入的兩個數字進行相加,然後返回這個請求,透過@SendTo傳送到Kafka的響應主題。


總結服務
現在,讓我們將所有這些都結合在一起放在RESTcontroller,步驟分為幾步,先建立生產者記錄,並在記錄頭部中設定接受響應的Kafka主題,這樣
把請求和響應在Kafka那裡對應起來,然後透過模板釋出訊息到Kafka,再透過future.get()堵塞等待Kafka的響應主題傳送響應結果過來。這時再
列印結果記錄中的頭部資訊,會看到Spring自動生成相關ID。

@ResponseBody
@PostMapping(value="/sum",produces=MediaType.APPLICATION_JSON_VALUE,consumes=MediaType.APPLICATION_JSON_VALUE)
public  Model  sum(@RequestBody  Model  request)throws InterruptedException,ExecutionException {
  //建立生產者記錄
  ProducerRecord<String,Model>  record  = new ProducerRecord<String,Model>(requestTopic,request);
  //在記錄頭部中設定響應主題
  record.headers().add(new RecordHeader(KafkaHeaders.REPLY_TOPIC, requestReplyTopic.getBytes()));
  //釋出到kafka主題中
  RequestReplyFuture<String, Model, Model> sendAndReceive = kafkaTemplate.sendAndReceive(record);

  //確認生產者是否成功生產
  SendResult<String, Model> sendResult = sendAndReceive.getSendFuture().get();
    
  //列印結果記錄中所有頭部資訊 會看到Spring自動生成的相關ID,這個ID是由消費端@SendTo 註釋返回的值。 
 sendResult.getProducerRecord().headers().forEach(header -> System.out.println(header.key() + ":" + header.value().toString()));
    
  //獲取消費者記錄
  ConsumerRecord<String, Model> consumerRecord = sendAndReceive.get();
    
  //返回消費者結果
  return consumerRecord.value();
}
<p class="indent">


併發消費者
即使你要建立請求主題在三個分割槽中,三個併發的消費者的響應仍然合併到一個Kafka響應主題,這樣,Spring偵聽器的容器能夠完成匹配相關ID的繁重工作。
整個請求/響應的模型是一致的。

現在我們可以再修改啟動類如下:

@ComponentScan(basePackages = {
        "com.gauravg.config",
        "com.gauravg.consumer",
        "com.gauravg.controller",
        "com.gauravg.model"
    })
@SpringBootApplication
public class RequestReplyKafkaApplication {

  public static void main(String[] args) {
    SpringApplication.run(RequestReplyKafkaApplication.class, args);
  }
}

<p class="indent">


下面開始執行這個案例:
1.下載原始碼見github
2.先啟動kafka
3.直接執行上面啟動類
4.透過postman等工具訪問:
http://localhost:8080/sum

post資料:

{
  "firstNumber": "111",
  "secondNumber": "2222"
}
<p class="indent">


返回結果是:

{
    "firstNumber": 111,
    "secondNumber": 2222,
    "sum": 2333
}
<p class="indent">


在控制檯輸出記錄頭部資訊:

kafka_replyTopic:[B@1f59b198
kafka_correlationId:[B@356a7326
__TypeId__:[B@1a9111f

<p class="indent">


可見,Spring自動生成聚合ID(correlationId),無需我們自己手工比對了。


Synchronous Kafka: Using Spring Request-Reply - DZ

[該貼被admin於2018-07-23 18:11修改過]

[該貼被admin於2018-07-23 18:13修改過]

相關文章