Springboot整合消息队列Kafka
一、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 。可参考博客 https://blog.csdn.net/lishuangzhe7047/article/details/74530417 理解
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
发送消息的方式。因为需要配置Producer
的acks = 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)]
- 通过日志我们可以看到,我们发送的消息,分别被
Demo01AConsumer
和Demo01Consumer
两个消费者(消费者分组)都消费了一次。 - 同时,两个消费者在不同的线程中,消费了这条消息。
执行 #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
注解。