Springboot整合消息队列Kafka

  • 一、Spring-Kafka
  • 二、快速入门
  • 2.1 引入依赖
  • 2.2 应用配置文件
  • 2.3 Application
  • 2.4 Demo01Message
  • 2.5 Demo01Producer
  • 2.6 Demo01Consumer
  • 2.7 Demo01AConsumer
  • 2.8 简单测试
  • 2.9 @KafkaListener


一、Spring-Kafka

Spring 生态中,提供了 Spring-Kafka 项目,让我们更简便的使用 Kafka

Spring for Apache Kafka (spring-kafka) 项目将 Spring 核心概念应用于基于 Kafka 的消息传递解决方案的开发。
它提供了一个“模板”作为发送消息的高级抽象。
它还通过 @KafkaListener 注解和“侦听器容器(listener container)”为消息驱动的 POJO 提供支持。
这些库促进了依赖注入和声明的使用。
在所有这些用例中,你将看到 Spring Framework 中的 JMS 支持,以及和 Spring AMQP 中的 RabbitMQ 支持的相似之处。

二、快速入门

先来对 Kafka-Spring 做一个快速入门,实现 Producer 三种发送消息的方式的功能,同时创建一个 Consumer 消费消息。

2.1 引入依赖
<dependencies>
    <!-- 引入 Spring-Kafka 依赖 -->
    <!-- 已经内置 kafka-clients 依赖,所以无需重复引入 -->
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
        <version>2.3.3.RELEASE</version>
    </dependency>

    <!-- 实现对 JSON 的自动化配置 -->
    <!-- 因为,Kafka 对复杂对象的 Message 序列化时,我们会使用到 JSON -->
    <!--
        同时,spring-boot-starter-json 引入了 spring-boot-starter ,而 spring-boot-starter 又引入了 spring-boot-autoconfigure 。
        spring-boot-autoconfigure 实现了 Spring-Kafka 的自动化配置
     -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-json</artifactId>
    </dependency>

    <!-- 方便等会写单元测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>
2.2 应用配置文件

resources 目录下,创建 application.yaml 配置文件。

spring:
  # Kafka 配置项,对应 KafkaProperties 配置类
  kafka:
    bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
    # Kafka Producer 配置项
    producer:
      acks: 1 # 0-不应答。1-leader 应答。all-所有 leader 和 follower 应答。
      retries: 3 # 发送失败时,重试发送的次数
      key-serializer: org.apache.kafka.common.serialization.StringSerializer # 消息的 key 的序列化
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 消息的 value 的序列化
    # Kafka Consumer 配置项
    consumer:
      auto-offset-reset: earliest # 设置消费者分组最初的消费进度为 earliest 。可参考博客  理解
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      # JsonDeserializer 在反序列化消息时,考虑到安全性,只反序列化成信任的 Message 类
      properties:
        spring:
          json:
            trusted:
              packages: cn.cy.springboot.kafkademo.message
    # Kafka Consumer Listener 监听器配置
    listener:
      missing-topics-fatal: false # 消费监听接口监听的主题不存在时,默认会报错。所以通过设置为 false ,解决报错

logging:
  level:
    org:
      springframework:
        kafka: ERROR # spring-kafka INFO 日志太多了,所以我们限制只打印 ERROR 级别
      apache:
        kafka: ERROR # kafka INFO 日志太多了,所以我们限制只打印 ERROR 级别
2.3 Application

创建 Application.java 类,配置 @SpringBootApplication 注解即可。

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
2.4 Demo01Message

cn.cy.springboot.kafkademo.message 包下,创建 Demo01Message 消息类,提供给当前示例使用。

@Data
public class Demo01Message {
    public static final String TOPIC = "DEMO_01";
    /**
     * 编号
     */
    private Integer id;
}
  • TOPIC 静态属性,我们设置该消息类对应 Topic 为 "DEMO_01"
2.5 Demo01Producer

cn.cy.springboot.kafkademo.producer 包下,创建 Demo01Producer 类,它会使用 Kafka-Spring 封装提供的 KafkaTemplate ,实现三种发送消息的方式。

@Component
public class Demo01Producer {
    @Resource
    private KafkaTemplate<Object, Object> kafkaTemplate;
    public SendResult syncSend(Integer id) throws ExecutionException, InterruptedException {
        // 创建 Demo01Message 消息
        Demo01Message message = new Demo01Message();
        message.setId(id);
        // 同步发送消息
        return kafkaTemplate.send(Demo01Message.TOPIC, message).get();
    }
    public ListenableFuture<SendResult<Object, Object>> asyncSend(Integer id) {
        // 创建 Demo01Message 消息
        Demo01Message message = new Demo01Message();
        message.setId(id);
        // 异步发送消息
        return kafkaTemplate.send(Demo01Message.TOPIC, message);
    }
}
  • TOPIC 静态属性,我们设置该消息类对应 Topic"DEMO_01"
  • #asyncSend(...) 方法,异步发送消息。在方法内部,会调用 KafkaTemplate#send(topic, data) 方法,异步发送消息,返回 Spring ListenableFuture 对象,一个可以通过监听执行结果的 Future 增强。
  • #syncSend(...) 方法,同步发送消息。在方法内部,也是调用 KafkaTemplate#send(topic, data) 方法,异步发送消息。不过,因为我们后面调用了 ListenableFuture 对象的 #get() 方法,阻塞等待发送结果,从而实现同步的效果。
  • 暂时未提供 oneway 发送消息的方式。因为需要配置 Produceracks = 0 ,才可以使用这种发送方式。当然,实际场景下,基本不会使用 oneway
  • 在序列化时,我们使用了 JsonSerializer 序列化 Message 消息对象,它会在 Kafka 消息 Headers__TypeId__ 上,值为 Message 消息对应的类全名
  • 在反序列化时,我们使用了 JsonDeserializer 序列化出 Message 消息对象,它会根据 Kafka 消息 Headers__TypeId__的值,反序列化消息内容成该 Message 对象。
2.6 Demo01Consumer

cn.cy.springboot.kafkademo.consumer 包下,创建 Demo01Consumer 类,消费消息。

@Component
public class Demo01Consumer {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @KafkaListener(topics = Demo01Message.TOPIC,
            groupId = "demo01-consumer-group-" + Demo01Message.TOPIC)
    public void onMessage(Demo01Message message) {
        logger.info("[onMessage][线程编号:{} 消息内容:{}]", Thread.currentThread().getId(), message);
    }
}
  • 在方法上,添加了 @KafkaListener 注解,声明消费的 Topic"DEMO_01" ,消费者分组是 "demo01-consumer-group-DEMO_01" 。一般情况下,我们建议一个消费者分组,仅消费一个 Topic 。这样做会有个好处:每个消费者分组职责单一,只消费一个 Topic
  • 方法参数,使用消费 Topic 对应的消息类即可。这里,我们使用了 Demo01Message
  • 虽然说,@KafkaListener 注解是方法级别的,还是建议一个类,对应一个方法,消费消息。
2.7 Demo01AConsumer

cn.cy.springboot.kafkademo.consumer 包下,创建 Demo01AConsumer 类,消费消息。

@Component
public class Demo01AConsumer {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @KafkaListener(topics = Demo01Message.TOPIC,
            groupId = "demo01-A-consumer-group-" + Demo01Message.TOPIC)
    public void onMessage(ConsumerRecord<Integer, String> record) {
        logger.info("[onMessage][线程编号:{} 消息内容:{}]", Thread.currentThread().getId(), record);
    }
}
  • 整体和 Demo01Consumer 是一致的,主要有两个差异点,也是为什么我们又额外创建了这个消费者的原因。
  • 差异一: 在方法上,添加了 @KafkaListener 注解,声明消费的 Topic还是"DEMO_01" ,消费者分组修改成"demo01-A-consumer-group-DEMO_01" 。这样,我们就可以测试 Kafka 集群消费的特性。
  • 集群消费(Clustering):集群消费模式下,相同 Consumer Group 的每个 Consumer 实例平均分摊消息。
  • 也就是说,如果我们发送一条 Topic 为 "DEMO_01" 的消息,可以分别被 "demo01-A-consumer-group-DEMO_01""demo01-consumer-group-DEMO_01" 都消费一次。
  • 但是,如果我们启动两个该示例的实例,则消费者分组 "demo01-A-consumer-group-DEMO_01""demo01-consumer-group-DEMO_01" 都会有多个 Consumer 示例。此时,我们再发送一条 Topic 为 "DEMO_01" 的消息,只会被 "demo01-A-consumer-group-DEMO_01" 的一个 Consumer 消费一次,也同样只会被 "demo01-A-consumer-group-DEMO_01" 的一个 Consumer 消费一次。
  • 差异二,方法参数,设置消费的消息对应的类不是 Demo01Message 类,而是 Kafka 内置的 ConsumerRecord 类。通过 ConsumerRecord 类,我们可以获取到消费的消息的更多信息,例如说消息的所属队列、创建时间等等属性,不过消息的内容(value)就需要自己去反序列化。当然,一般情况下,我们不会使用 ConsumerRecord 类。
2.8 简单测试

创建 Demo01ProducerTest 测试类,编写二个单元测试方法,调用 Demo01Producer 二个发送消息的方式。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class Demo01ProducerTest {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired
    private Demo01Producer producer;
    @Test
    public void testSyncSend() throws ExecutionException, InterruptedException {
        int id = (int) (System.currentTimeMillis() / 1000);
        SendResult result = producer.syncSend(id);
        logger.info("[testSyncSend][发送编号:[{}] 发送结果:[{}]]", id, result);
        // 阻塞等待,保证消费
        new CountDownLatch(1).await();
    }
    @Test
    public void testASyncSend() throws InterruptedException {
        int id = (int) (System.currentTimeMillis() / 1000);
        producer.asyncSend(id).addCallback(new ListenableFutureCallback<SendResult<Object, Object>>() {
            @Override
            public void onFailure(Throwable e) {
                logger.info("[testASyncSend][发送编号:[{}] 发送异常]]", id, e);
            }
            @Override
            public void onSuccess(SendResult<Object, Object> result) {
                logger.info("[testASyncSend][发送编号:[{}] 发送成功,结果为:[{}]]", id, result);
            }
        });
        // 阻塞等待,保证消费
        new CountDownLatch(1).await();
    }
}

执行 #testSyncSend()方法,测试同步发送消息。

2020-12-24 16:35:46.040  INFO 25132 --- [           main] c.c.s.k.producer.Demo01ProducerTest      : [testSyncSend][发送编号:[1608798945] 发送结果:[SendResult [producerRecord=ProducerRecord(topic=DEMO_01, partition=null, headers=RecordHeaders(headers = [RecordHeader(key = __TypeId__, value = [99, 110, 46, 99, 121, 46, 115, 112, 114, 105, 110, 103, 98, 111, 111, 116, 46, 107, 97, 102, 107, 97, 100, 101, 109, 111, 46, 109, 101, 115, 115, 97, 103, 101, 46, 68, 101, 109, 111, 48, 49, 77, 101, 115, 115, 97, 103, 101])], isReadOnly = true), key=null, value=Demo01Message(id=1608798945), timestamp=null), recordMetadata=DEMO_01-0@6]]]
2020-12-24 16:35:46.392  INFO 25132 --- [ntainer#0-0-C-1] c.c.s.k.consumer.Demo01AConsumer         : [onMessage][线程编号:18 消息内容:ConsumerRecord(topic = DEMO_01, partition = 0, leaderEpoch = 0, offset = 6, CreateTime = 1608798945874, serialized key size = -1, serialized value size = 17, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = Demo01Message(id=1608798945))]
2020-12-24 16:35:46.395  INFO 25132 --- [ntainer#1-0-C-1] c.c.s.kafkademo.consumer.Demo01Consumer  : [onMessage][线程编号:20 消息内容:Demo01Message(id=1608798945)]
  • 通过日志我们可以看到,我们发送的消息,分别被 Demo01AConsumerDemo01Consumer 两个消费者(消费者分组)都消费了一次。
  • 同时,两个消费者在不同的线程中,消费了这条消息。

执行 #testASyncSend() 方法,测试异步发送消息。
注意,不要关闭 #testSyncSend()单元测试方法,因为我们要模拟每个消费者集群,都有多个 Consumer 节点。

2020-12-24 16:41:17.765  INFO 19088 --- [ad | producer-1] c.c.s.k.producer.Demo01ProducerTest      : [testASyncSend][发送编号:[1608799277] 发送成功,结果为:[SendResult [producerRecord=ProducerRecord(topic=DEMO_01, partition=null, headers=RecordHeaders(headers = [RecordHeader(key = __TypeId__, value = [99, 110, 46, 99, 121, 46, 115, 112, 114, 105, 110, 103, 98, 111, 111, 116, 46, 107, 97, 102, 107, 97, 100, 101, 109, 111, 46, 109, 101, 115, 115, 97, 103, 101, 46, 68, 101, 109, 111, 48, 49, 77, 101, 115, 115, 97, 103, 101])], isReadOnly = true), key=null, value=Demo01Message(id=1608799277), timestamp=null), recordMetadata=DEMO_01-0@9]]]
2020-12-24 16:41:20.753  INFO 19088 --- [ntainer#0-0-C-1] c.c.s.k.consumer.Demo01AConsumer         : [onMessage][线程编号:18 消息内容:ConsumerRecord(topic = DEMO_01, partition = 0, leaderEpoch = 0, offset = 9, CreateTime = 1608799277614, serialized key size = -1, serialized value size = 17, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = Demo01Message(id=1608799277))]
  • #testSyncSend()方法执行的结果,是一致的。此时,我们打开 #testSyncSend()方法所在的控制台,会看到有消息消费的日志。说明,符合集群消费的机制:集群消费模式下,相同 Consumer Group 的每个 Consumer 实例平均分摊消息。
  • 不过如上的日志,也可能出现在 #testSyncSend() 方法所在的控制台,而不在 #testASyncSend() 方法所在的控制台。
2.9 @KafkaListener

@KafkaListener 注解的常用属性:

/**
 * 监听的 Topic 数组
 */
String[] topics() default {};

/**
 * 监听的 Topic 表达式
 */
String topicPattern() default "";

/**
 * @TopicPartition 注解的数组。每个 @TopicPartition 注解,可配置监听的 Topic、队列、消费的开始位置
 */
TopicPartition[] topicPartitions() default {};

/**
 * 消费者分组
 */
String groupId() default "";

/**
 * 使用消费异常处理器 KafkaListenerErrorHandler 的 Bean 名字
 */
String errorHandler() default "";

/**
 * 自定义消费者监听器的并发数
 */
String concurrency() default "";

/**
 * 是否自动启动监听器。默认情况下,为 true 自动启动。
 */
String autoStartup() default "";

/**
 * Kafka Consumer 拓展属性。
 */
String[] properties() default {};

@KafkaListener 注解的不常用属性:

/**
 * 唯一标识
 */
String id() default "";
/**
 * id 唯一标识的前缀
 */
String clientIdPrefix() default "";
/**
 * 当 groupId 未设置时,是否使用 id 作为 groupId
 */
boolean idIsGroup() default true;

/**
 * 使用的 KafkaListenerContainerFactory Bean 的名字。
 * 若未设置,则使用默认的 KafkaListenerContainerFactory Bean 。
 */
String containerFactory() default "";

/**
 * 所属 MessageListenerContainer Bean 的名字。
 */
String containerGroup() default "";

/**
 * 真实监听容器的 Bean 名字,需要在名字前加 "__" 。
 */
String beanRef() default "__listener";
  • @TopicPartition 注解
  • @PartitionOffset 注解
  • @KafkaListeners 注解,允许我们在其中,同时添加多个 @KafkaListener 注解。