spring集成kafka,实现一个topic可以被多个group消费

本文介绍如何在Spring Boot项目中使用Kafka实现同一Topic下不同消费组的消息消费。通过配置不同的消费组ID并利用@KafkaListener注解指定消费组,确保消息能够被不同消费组正确处理。

    由于新公司是做物联网的,公司刚起步,没什么项目,就是在做一些基础的服务的搭建,现在微服务这么火,可想而知,Spring Boot ,Spring Cloud 是必须要会的技能,而做物联网,把各种智能设备的数据采集上来,也避免不了要用到消息系统。所以我们的架构师从众多消息中间件中选出了kafka和mqtt。mqtt在物联网中的作用不言而去,我这边也在学,从mosquitto服务端搭建,到通过mqtt的java客户端paho 实现消息的生产和消费,不过用paho实现太过复杂,还是用springboot为我们提供的实现方式简便。no no,偏离主题了,今天主要说的是kafka。

    首先呢,kafka集群和zookeeper的集群的搭建我这就不赘述了,网上有很多,其实还是考研自己的linux水平,linux熟的 这些服务搭建 就是小菜一碟。

    kafka消费者有一个熟悉groupId 就是一个topic中的消息只能被同一个groupId的消费者中的一个消费者消费。

这个groupId,在配置消费者时指定。

但是问题来了,怎么实现让一个topic可以让不同group消费呢。

这个为也不会,哈哈

所以上网查了,看了一个说的特别好。

意思就是

goupid不要用配置文件配置的方式

细心的话,会发现@KafkaListener 注解,里面有一个containerFactory参数,就是让你指定容器工厂的

动手吧。

新建一个KafkaConsumerConfig类,代码如下,指定了两个容器,也就两个group

分别为kafkaListenerContainerFactory1和kafkaListenerContainerFactory2

import java.util.HashMap;
import java.util.Map;
 
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
 
@Configuration
public class KafkaConsumerConfig {
 
    private String brokers = "192.168.52.130:9092,192.168.52.131:9092,192.168.52.133:9092";
 
    private String group1 = "test1";
    private String group2 = "test2";
 
    @Bean
    public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory1() {
        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<String, String>();
        factory.setConsumerFactory(consumerFactory1());
        factory.setConcurrency(4);
        factory.getContainerProperties().setPollTimeout(4000);
        return factory;
    }
    
    @Bean
    public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory2() {
        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<String, String>();
        factory.setConsumerFactory(consumerFactory2());
        factory.setConcurrency(4);
        factory.getContainerProperties().setPollTimeout(4000);
        return factory;
    }
    
    public Map<String, Object> getCommonPropertis() {
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers);
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "100");
        properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "15000");
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, group1);
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
        return properties;
    }
    
 
    public ConsumerFactory<String, String> consumerFactory1() {
        Map<String, Object> properties = getCommonPropertis();
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, group1);
        return new DefaultKafkaConsumerFactory<String, String>(properties);
    }
    
    public ConsumerFactory<String, String> consumerFactory2() {
    	 Map<String, Object> properties = getCommonPropertis();
         properties.put(ConsumerConfig.GROUP_ID_CONFIG, group2);
         return new DefaultKafkaConsumerFactory<String, String>(properties);
    }
}

上面代码中,其实,很多配置项,你也可以直接用@value的方式,从配置文件中读取过来,那么需要修改参数值的时候,就直接更改配置文件就行了,这点相信就不用教了,不懂的网上一搜一堆。

最后,在@KafkaListener 中指定容器名称

@KafkaListener(id="test1",topics = "test-topic", containerFactory="kafkaListenerContainerFactory1")
@KafkaListener(id="test2",topics = "test-topic", containerFactory="kafkaListenerContainerFactory2")
启动,你就会发现,卧槽,还真可以
[           main] xxx     : Kafka version : 0.10.1.1
[           main] xxx     : Kafka commitId : f10ef2720b03b247
[           main] xxx     : Tomcat started on port(s): 82 (http)
[           main] xxx     : Started App in 3.913 seconds (JVM running for 4.321)
[    test2-0-C-1] xxx     : Discovered coordinator 192.168.52.133:9092 (id: 2147483644 rack: null) for group test2.
[    test1-0-C-1] xxx     : Discovered coordinator 192.168.52.131:9092 (id: 2147483645 rack: null) for group test1.

至此,就实现了多个customer不同group的功能,亲测有效。

参考:https://blog.youkuaiyun.com/caijiapeng0102/article/details/80765923

https://www.jianshu.com/p/6a44da908e48  高版本 在@KafkaListener 注解中有groupId属性可以设置


<think> 我们讨论的是在Spring Kafka中使用@KafkaListener监听多个Topic时的消息消费顺序问题。 核心点:Kafka本身能保证单个分区内消息的顺序性,但多个Topic(或同一个Topic多个分区)之间没有全局顺序保证。 在Spring Kafka中,当我们使用一个@KafkaListener方法监听多个Topic时,这些Topic的消息会由同一个消费者线程(或线程池)处理吗?这直接关系到消息的顺序性。 根据Spring Kafka的设计: 1. 默认情况下,每个@KafkaListener方法会创建一个KafkaConsumer,并由一个单独的线程(即Kafka的Consumer线程)来消费消息。 2. 但是,当这个监听器同时监听多个Topic时,这个消费者线程会从多个Topic的分区中拉取消息。由于这些消息来自不同的Topic和分区,它们被处理的实际顺序可能与消息产生的顺序不一致。 具体来说: - 同一个Topic的同一个分区内的消息,在消费时能保证顺序(因为同一个分区内的消息在Kafka中有序,且由同一个消费者线程处理)。 - 不同Topic之间的消息,或者同一个Topic不同分区的消息,消费顺序无法保证。因为消费者线程轮询拉取消息时,可能先拉取到TopicA的消息,再拉取到TopicB的消息,也可能反过来。 另外,如果使用并发消费(即设置`concurrency`参数大于1),那么每个监听器会创建多个消费者实例(每个实例一个线程),这些线程会分别处理不同分区的消息。这种情况下,不同分区的消息处理顺序更无法保证。 如何保证顺序? 1. 如果业务要求严格顺序,那么应该将需要顺序处理的消息发送到同一个Topic的同一个分区。因为Kafka只保证分区内的顺序性。 2. 如果多个Topic的消息需要按某种顺序处理,可能需要重新设计,例如将相关消息发送到同一个Topic,并使用分区键确保它们进入同一个分区。 在Spring Kafka中,一个监听多个Topic的@KafkaListener示例: ```java @KafkaListener(topics = {"topic1", "topic2"}, groupId = "my-group") public void listen(ConsumerRecord<String, String> record) { // 处理消息 // 注意:topic1和topic2的消息都会进入这个方法,但处理顺序无法保证 } ``` 总结: - 多个Topic的消息被同一个监听器方法消费时,消费顺序取决于消息到达消费者的顺序(即轮询拉取到的顺序),而不同Topic的消息到达顺序是不确定的。 - 因此,在监听多个Topic时,不能期望来自不同Topic的消息之间有任何顺序保证。 参考资料: [^1]: Spring Kafka官方文档中关于消息监听容器的说明,特别是关于并发消费的部分。 </think>在 Spring Kafka 中使用 `@KafkaListener` 监听多个 Topic 时,消息的消费顺序遵循以下核心原则: --- ### 一、消费顺序的核心规则 1. **分区内有序性(核心保证)** **Kafka 严格保证同一分区内的消息按写入顺序消费**。 例如:分区 `P0` 中消息 `A→B→C` 写入,消费顺序必为 `A→B→C`[^1]。 2. **跨分区/跨 Topic 无序性** **不同分区或不同 Topic 之间的消费顺序无法保证**。原因: - 消费者通过轮询(poll)从多个分区拉取消息 - 线程调度可能导致处理顺序与拉取顺序不一致 - 示例: ```java // 监听两个 Topic 的监听器 @KafkaListener(topics = {"orders", "payments"}) public void handle(ConsumerRecord<?,?> record) { // 可能先处理 payments 的消息,后处理 orders 的消息 } ``` 3. **并发消费时的顺序问题** 当设置 `concurrency > 1` 时(如 `@KafkaListener(id = "multi", topics = "orders", concurrency = "3")`): - 同一分区的消息仍由**固定线程**处理(保证分区内顺序) - 不同分区的消息可能被**不同线程并行处理**(顺序无法保证) --- ### 二、顺序性保障方案 若需严格保证跨 Topic/分区的顺序,需以下设计: #### 方案 1:共享分区键(推荐) ```java // 生产端:相同业务ID的消息指定相同分区键 kafkaTemplate.send("orders", order.getUserId(), order); kafkaTemplate.send("payments", order.getUserId(), payment); // 消费端:监听合并后的 Topic @KafkaListener(topics = "merged-topic") public void process(ConsumerRecord<String, Object> record) { // 通过 userId 关联的消息会进入同一分区 } ``` **原理**:将关联消息路由到**同一 Topic 的同一分区**,利用分区有序性保证顺序[^1]。 #### 方案 2:单分区 + 单消费者 ```java @Bean public ConcurrentKafkaListenerContainerFactory<String, String> factory() { ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConcurrency(1); // 强制单线程消费 return factory; } @KafkaListener(topics = {"orders", "payments"}, containerFactory = "factory") public void sequentialProcess(ConsumerRecord<?,?> record) { // 单线程顺序处理所有消息(性能低) } ``` **代价**:严重牺牲吞吐量,仅适用于低流量场景。 --- ### 三、顺序验证测试 ```java // 测试生产者:向两个 Topic 交叉发送消息 void sendTestMessages() { kafkaTemplate.send("topicA", "A1"); kafkaTemplate.send("topicB", "B1"); kafkaTemplate.send("topicA", "A2"); } // 监听器日志可能输出: // Received B1 (topicB) // Received A1 (topicA) <- 顺序错乱 // Received A2 (topicA) ``` --- ### 四、最佳实践建议 1. **避免依赖跨 Topic 顺序** 业务设计时尽量让关联消息在同一 Topic/分区 2. **监控消费延迟** 通过 `ConsumerAwareRebalanceListener` 监控分区分配情况: ```java factory.getContainerProperties().setConsumerRebalanceListener(rebalanceListener); ``` 3. **幂等消费** 处理消息时实现幂等逻辑,防止乱序导致业务异常: ```java @KafkaListener(topics = "orders") public void handleOrder(OrderMessage order) { if (orderRepository.exists(order.getId())) return; // 幂等检查 // 处理逻辑 } ``` > **关键结论**:在监听多个 Topic 时,Spring Kafka 仅保证**单个分区内**的消息顺序,跨分区或跨 Topic 的消息消费顺序不可控[^1]。需通过业务设计或架构调整满足顺序需求。 ---
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值