前言
kafka已经安装好,原理咱们也做了初步的探究。接下来springboot整合kafka就要开始了。这里我想说的是,整合mq的话绝不能生产者发个消息,消费者消费了然后完事了,这绝对不行。这算什么整合呢?整合至少要关注到以下问题,下面的问题都解决了,整合才算是有那么一点用的。尽可能地咱们要贴近实际的业务场景:
- 如何确保消息不丢失?
- 如何保证消息不会重复消费?
- 如何避免消息的积压,加快消费速度?
- 如何保证有序的消费?(当然,业务场景是有状态的前提下)
- ...
依赖、环境准备
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.4.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
springboot parent的版本选择2.2.6,spring-kakfa的版本选择2.4.6.RELEASE。
yml文件的配置如下:
spring:
# KAFKA
kafka:
# ָkafka服务器地址,可以指定多个
bootstrap-servers: ****:9092,****:9092, ****:9092
#=============== producer生产者配置 =======================
producer:
retries: 0
# 每次批量发送消息的数量
batch-size: 16384
# 缓存容量
buffer-memory: 33554432
# ָ指定消息key和消息体的编解码方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
acks: 1
# properties:
#partitioner:
#class: com.cmdc.config.CustomizePartitioner
#=============== consumer消费者配置 =======================
consumer:
#指定默认消费者的group id
group-id: test-app
#earliest
#当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
#latest
#当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
#none
#topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
auto-offset-reset: latest
enable-auto-commit: false # 消费时不自动提交,改为手动确认
auto-commit-interval: 100ms
#指定消费key和消息体的编解码方式
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
max-poll-records: 10 # 每次拉取10条
server:
port: 8888
这里各项配置的描述已经很清晰了,几个重点配置特别地说下:
producer.acks:
# acks=0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。
# acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
# acks=all或-1 :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。
consumer. enable-auto-commit:
假设你在代码里面配置了消费者的手动提交,那么这里千万不要再设置为true了,否则自相矛盾,会报错IllegalStateException。
consumer. max-poll-records:
批量拉取的时候最多拉取多少条
整合生产者
整合生产者的时候,咱们就不啰嗦了,这里直接采用带有回调的发送方式,即完成生产者的消息确认:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Component;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
/**
* @author : wuwensheng
* @date : 10:49 2021/12/20
*/
@Component
public class KafkaSender {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
private final Logger logger = LoggerFactory.getLogger(KafkaSender.class);
public void send(String topic, String taskid, String jsonStr) {
//发送消息
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, taskid, jsonStr);
future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
@Override
//推送成功
public void onSuccess(SendResult<String, Object> result) {
logger.info(topic + " 生产者 发送消息成功:" + result.toString());
logger.info("发送消息成功:" + result.getRecordMetadata().topic() + "-"
+ result.getRecordMetadata().partition() + "-" + result.getRecordMetadata().offset());
}
@Override
//推送失败
public void onFailure(Throwable ex) {
logger.info(topic + " 生产者 发送消息失败:" + ex.getMessage());
}
});
}
}
调用send()方法的时候需要传递topic、taskid以及数据,taskid也可以不传递,看情况进行选择就好。
另外kafkaTemplate可以自行定制,不一定要用来自springboot enableAutoConfigure自动配置的这个。
我们现在写一个接口,模仿瞎=下kafak生产者发送数据;
import com.alibaba.fastjson.JSONObject;
import com.cmdc.producer.KafkaSender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* @author : wuwensheng
* @date : 10:51 2021/12/20
*/
@RestController
public class KafkaController {
@Autowired
private KafkaSender kafkaSender;
@GetMapping("/sendMessageToKafka")
public String sendMessageToKafka() {
Map<String, String> messageMap = new HashMap();
messageMap.put("message", "我是一条消息");
String taskid = "123456";
String jsonStr = JSONObject.toJSONString(messageMap);
//kakfa的推送消息方法有多种,可以采取带有任务key的,也可以采取不带有的(不带时默认为null)
kafkaSender.send("testTopic", taskid, jsonStr);
return "hi guy!";
}
}
注意这个testTopic并没有提前创建,在发送的时候将自动创建,不过分区数量是1,副本数也是1而已。启动项目,咱们试一下:
发送成功,那么接下来搞个生产者将这个消息消费下,看能不能成功。
整合简单消费者
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Hello!
* Created By JCccc on 2018/11/24
* 13:13
*/
@Component
public class KafkaConsumer {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 下面的主题是一个数组,可以同时订阅多主题,只需按数组格式即可,也就是用“,”隔开
*
* @param record
*/
@KafkaListener(topics = {"testTopic"})
public void receive(ConsumerRecord<?, ?> record) {
logger.info("消费得到的消息---key: " + record.key());
logger.info("消费得到的消息---value: " + record.value().toString());
}
}
在发送一条消息,看看能否接收到:
ok,已经发送成功。
整合消费者(批量消费,带消息确认等)
提前创建topic,规划分区和副本
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.consumer.Consumer;
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.config.TopicBuilder;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.listener.ContainerProperties;
import org.springframework.kafka.listener.KafkaListenerErrorHandler;
import org.springframework.kafka.listener.ListenerExecutionFailedException;
import org.springframework.messaging.Message;
import javax.validation.constraints.NotNull;
/**
* @author : wuwensheng
* @date : 10:49 2021/12/20
*/
@Configuration
@Slf4j
public class KafkaInitialConfiguration {
private static final String TOPIC_FIRST = "testtopic1";
private static final String TOPIC_SECOND = "singleTopic";
/**
* 创建一个名为testtopic的Topic并设置分区数为8,分区副本数为2
* <p>
* 如果要修改分区数,只需要修改配置重启即可,修改分区数不会导致数据的丢失,但是分区数只能增大不能减少
*
* @return 创建的新topic
*/
@Bean
public NewTopic initialTopic() {
log.info("create new topic now");
return TopicBuilder.name(TOPIC_FIRST).partitions(8).replicas(2).build();
}
}
自行定制KafkaListenerContainerFactory
KafkaListenerContainerFactory可以帮助来配置消费者进行消费时的几乎所有细节,因为咱们想在消费的时候能完成消息确认,批量消费等,所以咱们自己来定制,如下:
@Bean("batch")
@NotNull
public KafkaListenerContainerFactory<?> batchFactory(ConsumerFactory<Integer, String> consumerFactory) {
// 这里的consumerFactory可以设置各种参数的
log.info("batch receive consumerFactory,is:{}", consumerFactory);
ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
// 批量拉取数据
factory.setBatchListener(true);
// 设置每个@KafkaListener的线程数
factory.setConcurrency(3);
// 设置手动提交ack,对于要求较为严格的业务较为合适
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
@Bean("single")
@NotNull
public KafkaListenerContainerFactory<?> singleFactory(ConsumerFactory<Integer, String> consumerFactory) {
log.info("single factory receive consumerFactory::{}", consumerFactory);
ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
// 批量拉取数据
factory.setBatchListener(false);
// 设置每个@KafkaListener的线程数
factory.setConcurrency(5);
// 设置手动提交ack,对于要求较为严格的业务较为合适
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
batch用于批量监听的消费者,single用于单条消费的消费者。我们对是否批量拉取数据以及手动ack确认消息的消费都做出设置了。详细的可以看看代码怎么做的。
定制异常处理器
@Bean
public KafkaListenerErrorHandler kafkaListenerErrorHandler() {
return new KafkaListenerErrorHandler() {
@Override
public Object handleError(Message<?> message, ListenerExecutionFailedException exception) {
return null;
}
@Override
public Object handleError(Message<?> message, ListenerExecutionFailedException exception, Consumer<?, ?> consumer) {
//do something
return null;
}
};
}
在消费者消费消息出现异常的时候可以通过异常处理器做出处理,但是由于咱们是手动ack的,异常可以自行处理,手动ack的时候这个异常处理器没什么作用了就。
批量消息消费者代码
/**
* @param recordList
* @param acknowledgment KafkaListener id :消费者线程的命名规则.
* groupId:指定该消费组的消费组名
* topics:指定要监听哪些topic 这个是从所有的分区取数据吗?
*/
@KafkaListener(id = "consumer2", groupId = "felix-group", topics = "testtopic1", containerFactory = "batch",
properties = {
ConsumerConfig.MAX_POLL_RECORDS_CONFIG + "=10",// 每次最多取10个进行消费
ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG + "=50000" // 如果超过5分钟pull不到数据,那么尝试自行拉取
}
)
public void onMessage3(List<ConsumerRecord<?, ?>> recordList, Acknowledgment acknowledgment) {
logger.info(">>>批量消费一次,records.size()=" + recordList.size());
logger.info("===================================================start");
try {
for (ConsumerRecord<?, ?> record : recordList) {
logger.info("get message from record:{}", record.value());
}
acknowledgment.acknowledge();
logger.info("====================================================end");
} catch (Exception e) {
logger.info("exception occur when consume message:{}", e.getMessage());
acknowledgment.acknowledge();
}
}
消费者所属的group是felix-group,每次拉取10个消息,使用的containerFactory是"batch"这个实例,无论是否抛出异常都进行了ack。我这里是简单的模拟,你完全可以在抛出异常的时候执行更加可靠和复杂的操作。
那么咱们玩一下,先把消费者注释掉。然后重启下项目:
紧接着往testTopic1这个队列发送一些消息。
@Test
public void test1() {
for (int i = 1; i < 101; i++) {
String replace = UUID.randomUUID().toString().replace("-", "");
kafkaSender.send("testtopic1", String.valueOf(i) + "|||" + replace, String.valueOf(i) + "|||" + replace);
}
}
发送完成之后把消费者的注释去掉,然后再重启下项目:
没问题的,可以看到消息消费的时候没有顺序哈,这很正常,因为存入的时候就是无序的,8个分区,消息去往哪个分区都有可能。消费的时候也是无序的,消费者线程不止一个,无法保证顺序。关于这个问题,后面咱们专门再讨论。
总结
对kafka的简单整合到此,后面咱们慢慢讨论复杂情形。