Spring整合Kafka(二)----producer发送消息

一、使用KafkaTemplate

KafkaTemplate 包装了一个producer并提供了方便的方法发送消息到kafka topic,以下是KafkaTemplate提供的发送相关的方法:

CompletableFuture<SendResult<K, V>> sendDefault(V data);

CompletableFuture<SendResult<K, V>> sendDefault(K key, V data);

CompletableFuture<SendResult<K, V>> sendDefault(Integer partition, K key, V data);

CompletableFuture<SendResult<K, V>> sendDefault(Integer partition, Long timestamp, K key, V data);

CompletableFuture<SendResult<K, V>> send(String topic, V data);

CompletableFuture<SendResult<K, V>> send(String topic, K key, V data);

CompletableFuture<SendResult<K, V>> send(String topic, Integer partition, K key, V data);

CompletableFuture<SendResult<K, V>> send(String topic, Integer partition, Long timestamp, K key, V data);

CompletableFuture<SendResult<K, V>> send(ProducerRecord<K, V> record);

CompletableFuture<SendResult<K, V>> send(Message<?> message);

Map<MetricName, ? extends Metric> metrics();

List<PartitionInfo> partitionsFor(String topic);

<T> T execute(ProducerCallback<K, V, T> callback);

// Flush the producer.

void flush();

interface ProducerCallback<K, V, T> {

    T doInKafka(Producer<K, V> producer);

}

sendDefault方法要求一个默认的topic提供给KafkaTemplate。
上面有方法是带有timestamp参数的,这个方法会把时间戳存储在消息记录里。在broker中,用户提供的timestamp是否被使用取决于kafka topic配置的message.timestamp.type参数,如果参数值为CreateTime,用户提供的timestamp会被使用,如果参数值为LogAppendTime,broker会将服务器本地时间填入。配置message.timestamp.type可以在创建topic时指定,如下:

kafka-topics.sh --zookeeper 127.0.0.1:2181/kafka \
--create \
--topic test.4 \
--partitions 1 --replication-factor 1 \
--config message.timestamp.type=CreateTime

metrics 和 partitionsFor 两个方法会直接调用Producer对应的方法。metrics 返回kafka的度量指标,partitionsFor 返回某个topic的分区和副本信息。execute方法可以接收一个lambda表达式,如下:

template.execute((e) -> e.send(new ProducerRecord<>("topicx","execute")));

如果你不打算使用Spring-kafka默认的producer参数,可以这样初始化和使用KafkaTemplate

    @Bean
    public ProducerFactory<Integer, String> kafkaProducerFactory() {
        Map<String, Object> producerProperties = new HashMap<>();
        producerProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092,localhost:9093");
        producerProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
        producerProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        producerProperties.put(ProducerConfig.LINGER_MS_CONFIG, 5000);
        producerProperties.put(ProducerConfig.BATCH_SIZE_CONFIG,20480);
        DefaultKafkaProducerFactory<Integer, String> factory = new DefaultKafkaProducerFactory<>(producerProperties);
        return factory;
    }
    @Bean
    public KafkaTemplate<Integer, String> kafkaTemplate() {
        return new KafkaTemplate<Integer, String>(kafkaProducerFactory());
    }
    @Bean
    public ApplicationRunner runner1(KafkaTemplate<Integer, String> template) {
        return (args) -> {
            System.out.println("-----------send test-----------");
            template.send("test1", "{'name':'test24010306'}");
        };
    }

ProducerFactory这个bean设置了producer的各项属性,包括bootstrap.servers,key.serializer,value.serializer,batch.size,linger.ms,这里batch.size=20kB,是指批量发送的消息大小以20KB为单位,linger.ms=5000ms是指消息延迟5秒发送,当batch.size和linger.ms中的一个条件先达到时,producer发送数据到broker。
默认情况下,KafkaTemplate配置了一个LoggingProducerListener,这个Listener会记录错误,但当发送成功时不做任何事情。
注意send方法返回的是CompletableFuture,你可以注册一个回调函数来异步接收发送的结果:

    @Bean
    public ApplicationRunner runner1(KafkaTemplate<Integer, String> template) {
        return (args) -> {
            String topic = "test1";
            String value = "{'name':'test24010309'}";
            CompletableFuture<SendResult<Integer, String>> future = template.send(topic, value);
            future.whenComplete((result, ex) -> {
                if (ex == null) {
                    //handleSuccess(result);
                }
                else {
                    //handleFailure(result, ex);
                }
            });
        };
    }

你可以调用future的get()方法,这将阻塞发送线程以同步的方式获取发送结果:

    @Bean
    public ApplicationRunner runner2(KafkaTemplate<Integer, String> template) {
        return (args) ->{
            String topic = "test1";
            String value = "{'name':'test24010310'}";
            try {
                ProducerRecord<Integer,String> record = new ProducerRecord<>(topic, value);
                SendResult<Integer, String> result = template.send(record).get(10, TimeUnit.SECONDS);
                //handleSuccess(result);
            }
            catch (ExecutionException e) {
                //handleFailure(topic,value, e.getCause());
            }
            catch (TimeoutException | InterruptedException e) {
                //handleFailure(topic,value, e);
            }
        };
    }

其中get()方法的超时时间为10秒。handleSuccess()为消息发送成功的处理逻辑,handleFailure()为消息发送失败的处理逻辑。

二、使用RoutingKafkaTemplate

使用RoutingKafkaTemplate可以在运行时根据topic名字选择不同的producer,这个RoutingKafkaTemplate不支持事务,execute, flush 和 metrics操作。RoutingKafkaTemplate的构造方法需要一个map作为参数,map的key为java.util.regex.Pattern(这个正则表达式匹配的是topic的名字),map的value为ProducerFactory<Object, Object>,另外这个map需要是有序的(LinkedHashMap),因为它会被顺序遍历。以下例子展示了RoutingKafkaTemplate选择不同的producer发送不同消息,每个producer配置了不同的value.serializer:

@SpringBootApplication
public class RoutingKafkaTemplateApplication {
    public static void main(String[] args) {
        SpringApplication.run(RoutingKafkaTemplateApplication.class, args);
    }
    @Bean
    public RoutingKafkaTemplate routingTemplate(GenericApplicationContext context,
                                                ProducerFactory<Object, Object> pf) {
        // 克隆producer factory,并且设置一个不同的value.serializer
        Map<String, Object> configs = new HashMap<>(pf.getConfigurationProperties());
        configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
        DefaultKafkaProducerFactory<Object, Object> valueIntPF = new DefaultKafkaProducerFactory<>(configs);
        Map<Pattern, ProducerFactory<Object, Object>> map = new LinkedHashMap<>();
        map.put(Pattern.compile("two"), valueIntPF);//克隆的producer factory的value.serializer配置了IntegerSerializer
        map.put(Pattern.compile(".+"), pf); // 默认 producer factory 的value.serializer配置了StringSerializer
        return new RoutingKafkaTemplate(map);
    }
    @Bean
    public ApplicationRunner runner(RoutingKafkaTemplate routingTemplate) {
        return args -> {
            routingTemplate.send("test1", "thing1");
            routingTemplate.send("two", 555);
        };
    }
}

三、使用DefaultKafkaProducerFactory

当我们使用KafkaTemplate,一个ProducerFactory会被用来创建producer。当不使用事务时,默认情况下DefaultKafkaProducerFactory会创建一个单例的producer被所有客户端线程使用,但是,当你调用KafkaTemplate的flush()时,使用同一个producer的其他线程会出现延迟的现象。现在DefaultKafkaProducerFactory 有一个新的属性producerPerThread,当设置为true,DefaultKafkaProducerFactory会为每个线程创建一个独立的producer,这样,前面提到的延迟现象就消失了。另外,当producerPerThread是true的时候,如果一个producer不再需要,用户代码必须调用DefaultKafkaProducerFactory的closeThreadBoundProducer(),这个方法会物理性关闭producer,并且把它从ThreadLocal移除。调用reset()和destroy() 不会清除这些producer。
当创建一个DefaultKafkaProducerFactory时,key.serializer和value.serializer可以通过配置参数(properties map)传入构造方法,也可以传Serializer实例到构造方法。再或者你可以提供Supplier到构造方法,它会被用来为每个producer获取单独的Serializer实例:

@Bean
public ProducerFactory<Integer, CustomValue> producerFactory() {
    return new DefaultKafkaProducerFactory<>(producerConfigs(), null, () -> new CustomValueSerializer());
}
@Bean
public KafkaTemplate<Integer, CustomValue> kafkaTemplate() {
    return new KafkaTemplate<Integer, CustomValue>(producerFactory());
}

现在,你可以在ProducerFactory创建之后更新producer的配置。这个也许会有用,比如credentials 发生了变更,你要更新ssl key/trust store locations。这些变更不会影响已存在的producer实例,需要调用reset()来关闭已存在的producer,然后新的producer会以新的配置被创建。注意,开启事务的producer factory不能被改为非事务的,反之亦然。ProducerFactory中更新配置和移除配置的方法如下:

void updateConfigs(Map<String, Object> updates);
void removeConfig(String configKey);

如果你以对象的方式提供serializer(通过构造方法或者setter),ProducerFactory会调用configure()方法以configuration properties配置serializer。

四、使用ReplyingKafkaTemplate

ReplyingKafkaTemplate 是KafkaTemplate 的子类,实现了request/reply语义,它新增的两个方法如下:

RequestReplyFuture<K, V, R> sendAndReceive(ProducerRecord<K, V> record);

RequestReplyFuture<K, V, R> sendAndReceive(ProducerRecord<K, V> record,
    Duration replyTimeout);

结果是CompletableFuture类型 ,这个类会被结果异步填充,结果还有一个sendFuture 属性,是调用KafkaTemplate.send()的结果。你可以用这个sendFuture 来确定发送操作的结果。
如果上面第一个方法被调用,replyTimeout 属性就为null,ReplyingKafkaTemplate的defaultReplyTimeout 属性会被使用,默认为5秒。
ReplyingKafkaTemplate有一个waitForAssignment方法非常有用,如果reply容器配置为auto.offset.reset=latest,它可以避免ReplyingKafkaTemplate在reply容器初始化之前发送请求和接收应答。
当使用手动分配分区时,waitForAssignment等待的时间必须大于reply容器的pollTimeout 属性,因为notification 要等到第一次poll 完成才发送。
以下代码展示了如何使用ReplyingKafkaTemplate,producer端:

@SpringBootApplication
public class KRequestingApplication {
    public static void main(String[] args) {
        SpringApplication.run(KRequestingApplication.class, args).close();
    }
    @Bean
    public ApplicationRunner runner(ReplyingKafkaTemplate<String, String, String> template) {
        return args -> {
            if (!template.waitForAssignment(Duration.ofSeconds(10))) {
                throw new IllegalStateException("Reply container did not initialize");
            }
            ProducerRecord<String, String> record = new ProducerRecord<>("kRequests", "foo");
            RequestReplyFuture<String, String, String> replyFuture = template.sendAndReceive(record);
            SendResult<String, String> sendResult = replyFuture.getSendFuture().get(10, TimeUnit.SECONDS);
            System.out.println("Sent ok: " + sendResult.getRecordMetadata());
            ConsumerRecord<String, String> consumerRecord = replyFuture.get(10, TimeUnit.SECONDS);
            System.out.println("Return value: " + consumerRecord.value());
        };
    }
    @Bean
    public ReplyingKafkaTemplate<String, String, String> replyingTemplate(
            ProducerFactory<String, String> pf,
            ConcurrentMessageListenerContainer<String, String> repliesContainer) {

        return new ReplyingKafkaTemplate<>(pf, repliesContainer);
    }
    @Bean
    public ConcurrentMessageListenerContainer<String, String> repliesContainer(
            ConcurrentKafkaListenerContainerFactory<String, String> containerFactory) {

        ConcurrentMessageListenerContainer<String, String> repliesContainer =
                containerFactory.createContainer("kReplies");
        repliesContainer.getContainerProperties().setGroupId("repliesGroup");
        repliesContainer.setAutoStartup(false);
        return repliesContainer;
    }
    @Bean
    public NewTopic kRequests() {
        return TopicBuilder.name("kRequests")
            .partitions(10)
            .replicas(2)
            .build();
    }
    @Bean
    public NewTopic kReplies() {
        return TopicBuilder.name("kReplies")
            .partitions(10)
            .replicas(2)
            .build();
    }
}

repliesContainer创建了一个reply容器,并且指定了kReplies作为reply topic
replying端:

@SpringBootApplication
public class KReplyingApplication {
    public static void main(String[] args) {
        SpringApplication.run(KReplyingApplication.class, args);
    }
    @KafkaListener(id="server", topics = "kRequests")
    @SendTo
    public String listen(String in) {
        System.out.println("Server received: " + in);
        return in.toUpperCase();
    }
    @Bean // 如果Jackson 在classpath,则不需要这个
    public MessagingMessageConverter simpleMapperConverter() {
        MessagingMessageConverter messagingMessageConverter = new MessagingMessageConverter();
        messagingMessageConverter.setHeaderMapper(new SimpleKafkaHeaderMapper());
        return messagingMessageConverter;
    }
}

replying端监听kRequests topic,收到消息后转为大写,将消息再发回到kReplies topic,这时producer端会打印“Return value: FOO”。
ReplyingKafkaTemplate使用默认的header KafKaHeaders.REPLY_TOPIC来指明reply要发往的topic。ReplyingKafkaTemplate尝试从配置好的reply容器发现reply topic和partition,如果reply容器被配置为监听一个单独的topic或者一个单独的TopicPartitionOffset,这些信息会被用来设置reply header;如果reply容器没有像前面这样配置,用户必须设置reply headers,以下代码设置了reply topic:

record.headers().add(new RecordHeader(KafkaHeaders.REPLY_TOPIC, "kReplies".getBytes()));

使用单个reply TopicPartitionOffset进行配置时,只要每个template监听在不同的分区上,你就可以对多个template使用相同的reply topic。使用单个reply topic进行配置时,每个template实例必须使用不同的group.id。在这种情况下,所有template实例都收到每条reply,但只有发送请求的实例才能找到correlation ID。这可能对auto-scaling有帮助,但会带来额外的网络流量开销和丢掉每条不需要的reply的小开销,当你使用前面所述的配置,建议你把 template的 sharedReplyTopic设置为true,它可以改变不需要的reply的日志记录级别到DEBUG,而不是ERROR。
以下的例子是配置reply容器来使用相同的shared reply topic:

@Bean
public ConcurrentMessageListenerContainer<String, String> replyContainer(
        ConcurrentKafkaListenerContainerFactory<String, String> containerFactory) {

    ConcurrentMessageListenerContainer<String, String> container = containerFactory.createContainer("topic2");
    container.getContainerProperties().setGroupId(UUID.randomUUID().toString()); // 唯一的UUID
    Properties props = new Properties();
    props.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); // 新的group不会消费到旧replies
    container.getContainerProperties().setKafkaConsumerProperties(props);
    return container;
}

如果你有多个客户端实例(template),并且你没有像前面叙述的方式来配置它们,每个template需要一个专用的reply topic。另一个方法是设置KafkaHeaders.REPLY_PARTITION,为每个template实例设置专用的分区。Header包含了一个four-byte的 int (big-endian)。replying服务器必须用这个头把reply路由到正确的分区(@KafkaListener在做这个)。在这种情况下,reply容器一定不能使用Kafka的group management功能,并且必须让reply容器监听在固定的分区(在ContainerProperties的构造方法中传入一个TopicPartitionOffset)。
DefaultKafkaHeaderMapper需要classpath中有Jackson。如果没有,消息转换器就没有header的mapper,所以需要创建一个MessagingMessageConverter,并且配置header mapper为SimpleKafkaHeaderMapper(如前面replying端代码)。
默认情况下3个headers被使用:

  • KafkaHeaders.CORRELATION_ID–用来将reply关联到一个request
  • KafkaHeaders.REPLY_TOPIC–用来告诉replying服务器回复到哪里
  • KafkaHeaders.REPLY_PARTITION(可选)–用来告诉replying服务器回复到哪个分区
    @KafkaListener使用这些header信息来路由reply。
    另外,你可以修改headers的名字,template有相应的3个属性,correlationHeaderName、 replyTopicHeaderName 和 replyPartitionHeaderName。如果你的replying服务器不是spring应用,或者没有使用@KafkaListener,你就可以定制3个header的名字。

ReplyingKafkaTemplate有几个发送并接收方法使用了spring-messaging的 Message<?> 的参数。

RequestReplyMessageFuture<K, V> sendAndReceive(Message<?> message);

<P> RequestReplyTypedMessageFuture<K, V, P> sendAndReceive(Message<?> message, ParameterizedTypeReference<P> returnType);

以上方法使用了默认的replyTimeout,还有重载的方法可以接收一个超时值。
如果consumer的Deserializer或者template的MessageConverter可以通过配置或者reply消息中的类型元数据在没有额外信息的情况下可以转换payload,使用第一个方法。
如果需要为返回类型提供类型信息,使用第二个方法,来协助消息转换器。这也允许同一个template接收不同的类型,即使reply中没有类型元数据,例如replying服务器不是spring应用时。
以下是调用第二个方法的例子:

@Bean
ReplyingKafkaTemplate<String, String, String> template(
        ProducerFactory<String, String> pf,
        ConcurrentKafkaListenerContainerFactory<String, String> factory) {

    ConcurrentMessageListenerContainer<String, String> replyContainer =
            factory.createContainer("replies");
    replyContainer.getContainerProperties().setGroupId("request.replies");
    ReplyingKafkaTemplate<String, String, String> template =
            new ReplyingKafkaTemplate<>(pf, replyContainer);
    template.setMessageConverter(new ByteArrayJsonMessageConverter());
    template.setDefaultTopic("requests");
    return template;
}
RequestReplyTypedMessageFuture<String, String, Thing> future1 =
        template.sendAndReceive(MessageBuilder.withPayload("getAThing").build(),
                new ParameterizedTypeReference<Thing>() { });
log.info(future1.getSendFuture().get(10, TimeUnit.SECONDS).getRecordMetadata().toString());
Thing thing = future1.get(10, TimeUnit.SECONDS).getPayload();
log.info(thing.toString());

RequestReplyTypedMessageFuture<String, String, List<Thing>> future2 =
        template.sendAndReceive(MessageBuilder.withPayload("getThings").build(),
                new ParameterizedTypeReference<List<Thing>>() { });
log.info(future2.getSendFuture().get(10, TimeUnit.SECONDS).getRecordMetadata().toString());
List<Thing> things = future2.get(10, TimeUnit.SECONDS).getPayload();
things.forEach(thing1 -> log.info(thing1.toString()));

在replying服务器端,当@KafkaListener返回一个Message<?>类型的消息,框架会检测headers是否丢失,并且使用topic填充他们,这个topic可能来自@SendTo的定义或者接收到的KafkaHeaders.REPLY_TOPIC。框架还会返回KafkaHeaders.CORRELATION_ID和KafkaHeaders.REPLY_PARTITION。

@KafkaListener(id = "requestor", topics = "request")
@SendTo  // 此处没有定义topic,会使用 REPLY_TOPIC header
public Message<?> messageReturn(String in) {
    return MessageBuilder.withPayload(in.toUpperCase())
            .setHeader(KafkaHeaders.KEY, 42)
            .build();
}

五、使用AggregatingReplyingKafkaTemplate

ReplyingKafkaTemplate是严格应对用户单条request/reply的场景。面对多个接收者接收同一条数据并返回消息的场景,你可以使用AggregatingReplyingKafkaTemplate。这是客户端的Scatter-Gather Enterprise Integration Pattern的实现。
Scatter-Gather Enterprise Integration Pattern

类似于ReplyingKafkaTemplate,AggregatingReplyingKafkaTemplate的构造方法接收producer factory和listener container;AggregatingReplyingKafkaTemplate还有第三个参数BiPredicate<List<ConsumerRecord<K, R>>, Boolean> releaseStrategy,这个BiPredicate在每次收到reply的时候都会被执行一次;当predicate返回为true的时候,ConsumerRecord的集合被用于完成sendAndReceive返回的Future。
还有一个叫做returnPartialOnTimeout的属性,默认值是false,当它被设置为true的时候一个partial result正常完成了future(只要收到了一条reply的消息),而不是以KafkaReplyTimeoutException完成future。
当returnPartialOnTimeout被谁知为true并且发生了超时,predicate会被执行。第一个参数是当前的记录列表,第二个参数是true。predicate可以修改记录列表。

AggregatingReplyingKafkaTemplate<Integer, String, String> template =
        new AggregatingReplyingKafkaTemplate<>(producerFactory, container,
                        coll -> coll.size() == releaseSize);
...
RequestReplyFuture<Integer, String, Collection<ConsumerRecord<Integer, String>>> future =
        template.sendAndReceive(record);
future.getSendFuture().get(10, TimeUnit.SECONDS); // send ok
ConsumerRecord<Integer, Collection<ConsumerRecord<Integer, String>>> consumerRecord =
        future.get(30, TimeUnit.SECONDS);

注意,返回值是ConsumerRecord,它的value是ConsumerRecords的集合。外部的ConsumerRecord不是真的record,它是被template合成的做为一个请求的实际reply records的持有者。当一个正常的release发生(releaseStrategy返回true),topic被设置为aggregatedResults;如果returnPartialOnTimeout被设置为true,并且发生了超时,然后至少收到了一条reply,topic被设置为partialResultsAfterTimeout。template为这些topic名称提供了constant static变量:

/**
 * Pseudo topic name for the "outer" {@link ConsumerRecords} that has the aggregated
 * results in its value after a normal release by the release strategy.
 */
public static final String AGGREGATED_RESULTS_TOPIC = "aggregatedResults";
/**
 * Pseudo topic name for the "outer" {@link ConsumerRecords} that has the aggregated
 * results in its value after a timeout.
 */
public static final String PARTIAL_RESULTS_AFTER_TIMEOUT_TOPIC = "partialResultsAfterTimeout";

内层的集合中的ConsumerRecords包含了接收到了reply的真正topic的名字。
listener容器必须为reply配置成AckMode.MANUAL或者 AckMode.MANUAL_IMMEDIATE;consumer的enable.auto.commit属性设置为false,为了避免任何丢失消息的可能性,template只在没有未处理请求的时候提交偏移,例如,在最后一个未决请求被releaseStrategy释放的时候。在rebalance发生之后,有可能会有重复的reply,这些会被任何正在发生的请求忽略;当收到已发布的reply的重复消息时你可能看到错误日志。
如果你使用了ErrorHandlingDeserializer配合AggregatingReplyingKafkaTemplate,框架不会自动检测DeserializationException。相反,带有null值的记录会原封不动地返回,headers中会出现deserialization异常。建议程序调用工具方法ReplyingKafkaTemplate.checkDeserialization() 来确定是否有deserialization异常发生。replyErrorChecker也没有被template调用,你需要在reply的每个元素上做检查。

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值