Kafka 是一个由 LinkedIn 开发的分布式消息系统,详细介绍可以查看之前的文章。本文演示如何在 Spring Boot 项目中集成并使用 Kafka。
关于 Kafka 和 ZooKeeper 的安装 省略
(1)首先编辑项目的 pom.xml 文件,添加 spring-kafka 依赖:
org.springframework.kafka
spring-kafka
(2)然后在 application.properties 中添加 Kafka 相关配置:
提示:这里我们直接使用 Kafka 提供的 StringSerializer 和 StringDeserializer 进行数据的序列化和反序列化,然后使用 json 作为标准的数据传输格式。
虽然我们也可以自定义序列化和反序列化器进行实体类的序列化和反序列化,但实现起来十分麻烦,而且还有很多类型不支持,非常脆弱。复杂类型的支持更是一件痛苦的事情,不同版本之间的兼容性问题更是一个极大的挑战。由于 Serializer 和 Deserializer 影响到上下游系统,导致牵一发而动全身。所以建议直接使用 Kafka 提供的序列化和反序列化类。
###########【Kafka集群】###########
spring.kafka.bootstrap-servers=192.168.60.133:9092
###########【初始化生产者配置】###########
# 重试次数
spring.kafka.producer.retries=0
# 应答级别:多少个分区副本备份完成时向生产者发送ack确认(可选0、1、all/-1)
spring.kafka.producer.acks=1
# 批量大小
spring.kafka.producer.batch-size=16384
# 提交延时
spring.kafka.producer.properties.linger.ms=0
# 当生产端积累的消息达到batch-size或接收到消息linger.ms后,生产者就会将消息提交给kafka
# linger.ms为0表示每接收到一条消息就提交给kafka,这时候batch-size其实就没用了
# 生产端缓冲区大小
spring.kafka.producer.buffer-memory = 33554432
# Kafka提供的序列化和反序列化类
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
# 自定义分区器
# spring.kafka.producer.properties.partitioner.class=com.felix.kafka.producer.CustomizePartitioner
###########【初始化消费者配置】###########
# 默认的消费组ID
spring.kafka.consumer.properties.group.id=defaultConsumerGroup
# 是否自动提交offset
spring.kafka.consumer.enable-auto-commit=true
# 提交offset延时(接收到消息后多久提交offset)
spring.kafka.consumer.auto.commit.interval.ms=1000
# 当kafka中没有初始offset或offset超出范围时将自动重置offset
# earliest:重置为分区中最小的offset;
# latest:重置为分区中最新的offset(消费分区中新产生的数据);
# none:只要有一个分区不存在已提交的offset,就抛出异常;
spring.kafka.consumer.auto-offset-reset=latest
# 消费会话超时时间(超过这个时间consumer没有发送心跳,就会触发rebalance操作)
spring.kafka.consumer.properties.session.timeout.ms=120000
# 消费请求超时时间
spring.kafka.consumer.properties.request.timeout.ms=180000
# Kafka提供的序列化和反序列化类
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
# 消费端监听的topic不存在时,项目启动会报错(关掉)
spring.kafka.listener.missing-topics-fatal=false
# 设置批量消费
# spring.kafka.listener.type=batch
# 批量消费每次最多消费多少条消息
# spring.kafka.consumer.max-poll-records=50
附:基本用法
1,创建生产者
消息发送主要是使用 KafkaTemplate,它具有多个方法可以发送消息,这里我们使用最简单的:
@RestController
public class KafkaProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 发送消息
@GetMapping("/test/{message}")
public void sendMessage1(@PathVariable("message") String normalMessage) {
kafkaTemplate.send("topic1", normalMessage);
}
}
2,创建消费者
监听器主要是使用 @KafkaListenter 注解即可,可以监听多个 topic 也可以监听单个(多个的话用逗号隔开):
@Component
public class KafkaConsumer {
// 消费监听
@KafkaListener(topics = {"topic1"})
public void listen1(String data) {
System.out.println(data);
}
}
我们发送消息的时候并没有事先创建相应的 Topic。这是因为 KafkaTemplate 在发送的时候就已经帮我们完成了创建的操作。
但这样也会存在一些问题,比如这种情况创建出来的 Topic 的 Partition(分区))数永远只有 1 个,也不会有副本,这就导致了我们在后期不能顺利扩展。所以有时我们还是有必要使用代码手动去创建 Topic
(1)使用 @Bean 注解创建 Topic 十分简单,我们可以在项目中新建一个配置类专门用来初始化 topic,代码如下:
@Configuration
public class KafkaInitialConfiguration {
//创建TopicName为topic.hangge.initial的Topic并设置分区数为8以及副本数为1
@Bean
public NewTopic initialTopic() {
return new NewTopic("topic.hangge.initial",8, (short) 1);
}
}
(2)项目启动后,使用工具可以看到 Topic 创建成功:
(1)如果要修改分区数,只需修改配置值重启项目即可:
注意:修改分区数并不会导致数据的丢失,但是分区数只能增大不能减。
@Configuration
public class KafkaInitialConfiguration {
//创建TopicName为topic.hangge.initial的Topic并设置分区数为10以及副本数为1
@Bean
public NewTopic initialTopic() {
return new NewTopic("topic.hangge.initial",10, (short) 1 );
}
}
(2)重启项目,可以看到分区数已经成功变成了 10:
(1)首先我们在配置类中注册 AdminClient 这个 Bean:
@Configuration
public class KafkaInitialConfiguration {
@Value("${spring.kafka.bootstrap-servers}")
private String kafkaServers;
@Bean
public AdminClient adminClient() {
Map props = new HashMap<>();
//配置Kafka实例的连接地址
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaServers);
return AdminClient.create(props);
}
}
(2)然后在需要的地方注入 AdminClient 即可以查询 Topic 信息,这里我们使用 lambda 表达式遍历输出:
AdminClient 除了查询 Topic 外,还有如下其他功能:
创建 Topic:createTopics(Collection newTopics)
删除 Topic:deleteTopics(Collection topics)
罗列所有 Topic:listTopics()
查询 Topic:describeTopics(Collection topicNames)
查询集群信息:describeCluster()
查询 ACL 信息:describeAcls(AclBindingFilter filter)
创建 ACL 信息:createAcls(Collection acls)
删除 ACL 信息:deleteAcls(Collection filters)
查询配置信息:describeConfigs(Collection resources)
修改配置信息:alterConfigs(Map configs)
修改副本的日志目录:alterReplicaLogDirs(Map replicaAssignment)
查询节点的日志目录信息:describeLogDirs(Collection brokers)
查询副本的日志目录信息:describeReplicaLogDirs(Collection replicas)
增加分区:createPartitions(Map newPartitions)
@RestController
public class HelloController {
@Autowired
private AdminClient adminClient;
@GetMapping("/hello")
public void hello() throws ExecutionException, InterruptedException{
DescribeTopicsResult result = adminClient.describeTopics(
Arrays.asList("topic.hangge.initial"));
result.all().get().forEach((k,v)->System.out.println("k: "+k+" ,v: "+v.toString()+"\n"));
}
}
(3)项目启动后访问 /hello 接口,可以看到控制台输出如下内容(里面包含了各个分区的信息等等):
(1)在之前的文章中我们都是通过 KafkaTemplate 的 send() 方法指定一个 topic 发送消息,其实 send() 方法还支持其他参数,具体如下:
参数说明:
topic:这里填写的是 Topic 的名字
partition:这里填写的是分区的 id,其实也是就第几个分区,id 从 0 开始。表示指定发送到该分区中
timestamp:时间戳,一般默认当前时间戳
key:消息的键
data:消息的数据
ProducerRecord:消息对应的封装类,包含上述字段
Message>:Spring 自带的 Message 封装类,包含消息及消息头
ListenableFuture> send(String topic, V data);
ListenableFuture> send(String topic, K key, V data);
ListenableFuture> send(String topic, Integer partition, K key, V data);
ListenableFuture> send(String topic, Integer partition, Long timestamp, K key, V data);
ListenableFuture> send(ProducerRecord record);
ListenableFuture> send(Message> message);
(2)下面是一些简单的使用样例:
//发送带有时间戳的消息
kafkaTemplate.send("topic.hangge.demo", 0, System.currentTimeMillis(), "key1", "message");
//使用ProducerRecord发送消息
ProducerRecord record = new ProducerRecord("topic.hangge.demo", "message");
kafkaTemplate.send(record);
//使用Message发送消息
Map map = new HashMap();
map.put(KafkaHeaders.TOPIC, "topic.hangge.demo");
map.put(KafkaHeaders.PARTITION_ID, 0);
map.put(KafkaHeaders.MESSAGE_KEY, 0);
GenericMessage message = new GenericMessage("use Message to send message",new MessageHeaders(map));
kafkaTemplate.send(message);
(1)sendDefault() 方法和 send() 方法类似,只不过它不需要传入 topic(直接使用默认 topic),该方法支持如下几种形式:
参数说明:
partition:这里填写的是分区的 id,其实也是就第几个分区,id 从 0 开始。表示指定发送到该分区中
timestamp:时间戳,一般默认当前时间戳
key:消息的键
data:消息的数据
ListenableFuture> sendDefault(V data);
ListenableFuture> sendDefault(K key, V data);
ListenableFuture> sendDefault(Integer partition, K key, V data);
ListenableFuture> sendDefault(Integer partition, Long timestamp, K key, V data);
(2)要使用 sendDefault 发送消息,首先我们需要创建一个配置类并编写一个带有默认 Topic 参数(高亮部分)的 KafkaTemplate:
@Configuration
public class KafkaProducerConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String servers;
@Value("${spring.kafka.producer.retries}")
private int retries;
@Value("${spring.kafka.producer.acks}")
private String acks;
@Value("${spring.kafka.producer.batch-size}")
private int batchSize;
@Value("${spring.kafka.producer.properties.linger.ms}")
private int linger;
@Value("${spring.kafka.producer.buffer-memory}")
private int bufferMemory;
public Map producerConfigs() {
Map props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(ProducerConfig.RETRIES_CONFIG, retries);
props.put(ProducerConfig.ACKS_CONFIG, acks);
props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
props.put(ProducerConfig.LINGER_MS_CONFIG, linger);
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return props;
}
public ProducerFactory producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
@Bean
public KafkaTemplate kafkaTemplate() {
KafkaTemplate template = new KafkaTemplate(producerFactory());
template.setDefaultTopic("topic.hangge.default"); // 设置默认的 topic
return template;
}
}
(3)然后我们调用 sendDefault 方法发送数据,虽然没有指定 topic,但 kafkaTemplate 会自动把消息发送到默认的 Topic 中:
@RestController
public class KafkaProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 发送消息
@GetMapping("/test")
public void test() {
kafkaTemplate.sendDefault("send default test");
}
}
当我们发送消息到 Kafka 后,有时我们需要确认消息是否发送成功,如果消息发送失败,就要重新发送或者执行对应的业务逻辑。下面分别演示如何在异步或者同步发送消息时,获取发送结果。
(1)默认情况下 KafkaTemplate 发送消息是采取异步方式,并且 kafkaTemplate 提供了一个回调方法 addCallback,我们可以在回调方法中监控消息是否发送成功或在失败时做补偿处理:
@RestController
public class KafkaProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 发送消息
@GetMapping("/test")
public void test() {
kafkaTemplate.send("topic1", "消息回调测试").addCallback(success -> {
// 消息发送到的topic
String topic = success.getRecordMetadata().topic();
// 消息发送到的分区
int partition = success.getRecordMetadata().partition();
// 消息在分区内的offset
long offset = success.getRecordMetadata().offset();
System.out.println("发送消息成功:" + topic + "-" + partition + "-" + offset);
}, failure -> {
System.out.println("发送消息失败:" + failure.getMessage());
});
}
}
(1)默认情况下 KafkaTemplate 发送消息是采取异步方式发送的,如果希望同步发送消息只需要在 send 方法后面调用 get 方法即可,get 方法返回的即为结果(如果发送失败则抛出异常)
@RestController
public class KafkaProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 发送消息
@GetMapping("/test")
public void test() {
try {
// 同步发送消息
SendResult sendResult =
kafkaTemplate.send("topic1", "消息回调测试").get();
// 消息发送到的topic
String topic = sendResult.getRecordMetadata().topic();
// 消息发送到的分区
int partition = sendResult.getRecordMetadata().partition();
// 消息在分区内的offset
long offset = sendResult.getRecordMetadata().offset();
System.out.println("发送消息成功:" + topic + "-" + partition + "-" + offset);
} catch (InterruptedException e) {
System.out.println("发送消息失败:" + e.getMessage());
} catch (ExecutionException e) {
System.out.println("发送消息失败:" + e.getMessage());
}
}
}
(2)get 方法还有一个重载方法 get(long timeout, TimeUnit unit),当 send 方法耗时大于 get 方法所设定的参数时会抛出一个超时异常。比如下面我们设置了超时时长为 1 微秒(肯定超时):
注意:虽然超时了,但仅仅是抛出异常,消息还是会发送成功的。
@RestController
public class KafkaProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 发送消息
@GetMapping("/test")
public void test() {
try {
// 同步发送消息(并且耗时限制在1ms)
SendResult sendResult =
kafkaTemplate.send("topic1", "消息回调测试").get(1, TimeUnit.MICROSECONDS);
// 消息发送到的topic
String topic = sendResult.getRecordMetadata().topic();
// 消息发送到的分区
int partition = sendResult.getRecordMetadata().partition();
// 消息在分区内的offset
long offset = sendResult.getRecordMetadata().offset();
System.out.println("发送消息成功:" + topic + "-" + partition + "-" + offset);
} catch (TimeoutException e) {
System.out.println("发送消息超时");
} catch (InterruptedException e) {
System.out.println("发送消息失败:" + e.getMessage());
} catch (ExecutionException e) {
System.out.println("发送消息失败:" + e.getMessage());
}
}
}
Kafka 同数据库一样支持事务,当发生异常或者出现特定逻辑判断的时候可以进行回滚,确保消息监听器不会接收到一些错误的或者不需要的消息。Kafka 使用事务有两种方式,下面分别进行介绍。
(1)通常情况下,如果不声明事务,即使发送消息后面报错了,前面消息也已经发送成功:
@RestController
public class KafkaProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 发送消息
@GetMapping("/test")
public void test() {
kafkaTemplate.send("topic1", "test executeInTransaction");
throw new RuntimeException("fail");
}
}
(2)我们可以使用 KafkaTemplate 的 executeInTransaction 方法来声明事务。这种方式开启事务是不需要配置事务管理器的,也可以称为本地事务。
@RestController
public class KafkaProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 发送消息
@GetMapping("/test")
public void test() {
// 声明事务:后面报错消息不会发出去
kafkaTemplate.executeInTransaction(operations -> {
operations.send("topic1","test executeInTransaction");
throw new RuntimeException("fail");
});
}
}
(1)如果要使用注解方式开启事务,首先我们需要配置 KafkaTransactionManager,这个类就是 Kafka 提供给我们的事务管理类,我们需要使用生产者工厂来创建这个事务管理类。
注意:我们需要在 producerFactory 中开启事务功能,并设置 TransactionIdPrefix,TransactionIdPrefix 是用来生成 Transactional.id 的前缀。
@Configuration
public class KafkaProducerConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String servers;
@Value("${spring.kafka.producer.retries}")
private int retries;
@Value("${spring.kafka.producer.acks}")
private String acks;
@Value("${spring.kafka.producer.batch-size}")
private int batchSize;
@Value("${spring.kafka.producer.properties.linger.ms}")
private int linger;
@Value("${spring.kafka.producer.buffer-memory}")
private int bufferMemory;
public Map producerConfigs() {
Map props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(ProducerConfig.RETRIES_CONFIG, retries);
props.put(ProducerConfig.ACKS_CONFIG, acks);
props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
props.put(ProducerConfig.LINGER_MS_CONFIG, linger);
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return props;
}
public ProducerFactory producerFactory() {
DefaultKafkaProducerFactory factory = new DefaultKafkaProducerFactory<>(producerConfigs());
factory.transactionCapable();
factory.setTransactionIdPrefix("tran-");
return factory;
}
@Bean
public KafkaTransactionManager transactionManager() {
KafkaTransactionManager manager = new KafkaTransactionManager(producerFactory());
return manager;
}
}
(2)之后如果一个方法需要使用事务,我们只需要在该方法上添加 @Transactional 注解即可:
@RestController
public class KafkaProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 发送消息
@GetMapping("/test")
@Transactional
public void test() {
kafkaTemplate.send("topic1","test executeInTransaction");
throw new RuntimeException("fail");
}
}