(256条消息) 看完这篇Kafka,你也许就会了Kafka_心的步伐的博客-CSDN博客
需要注意消费者offset的配置,生产者ack
记得修改ip地址
docker pull wurstmeister/kafka
docker pull wurstmeister/zookeeper
# 首先需要启动zookeeper
docker run -it --name zookeeper -p 12181:2181 -d wurstmeister/zookeeper:latest
# 第一台
docker run -it --name kafka01 -p 19092:9092 -d -e KAFKA_BROKER_ID=0 -e KAFKA_ZOOKEEPER_CONNECT=192.168.16.131:12181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.16.131:19092 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:latest
# 第二台
docker run -it --name kafka02 -p 19093:9092 -d -e KAFKA_BROKER_ID=1 -e KAFKA_ZOOKEEPER_CONNECT=192.168.16.131:12181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.16.131:19093 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:latest
# 第三台
docker run -it --name kafka03 -p 19094:9092 -d -e KAFKA_BROKER_ID=2 -e KAFKA_ZOOKEEPER_CONNECT=192.168.16.131:12181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.16.131:19094 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:latest
org.springframework.boot
spring-boot-starter-parent
3.1.1
org.springframework.kafka
spring-kafka
3.0.7
com.alibaba
fastjson
1.2.78
server:
port: 8088
kafkaserver:
server: 192.168.16.131:19092,192.168.16.131:19093,192.168.16.131:19094
topic: dume-topic
parttition: 0
parttition1: 1
group-id: dume
package com.example.kafkademo.config;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.NewTopic;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.KafkaAdmin;
import java.util.HashMap;
import java.util.Map;
/**
* @description
* @author: gdg
* @Date: 2023/7/13
*/
@Configuration
public class KafkaTopicConfig {
@Value("${kafkaserver.server}")
private String boardServer;
@Value("${kafkaserver.topic}")
private String topic;
/**
* 定义一个KafkaAdmin的bean,可以自动检测集群中是否存在topic,不存在则创建
*/
@Bean
public KafkaAdmin kafkaAdmin() {
Map configs = new HashMap<>();
// 指定多个kafka集群多个地址,例如:192.168.2.11,9092,192.168.2.12:9092,192.168.2.13:9092
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, boardServer);
return new KafkaAdmin(configs);
}
/**
* 创建Topic
* */
@Bean
public NewTopic newTopic(){
// 创建topic,需要指定创建的topic的"名称"、"分区数"、"副本数量(副本数数目设置要小于Broker数量)"
return new NewTopic(topic,3,(short) 1);
}
}
package com.example.kafkademo.config;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.InvalidRecordException;
import org.apache.kafka.common.PartitionInfo;
import java.util.List;
import java.util.Map;
/**
* @description <自定义分区规则>
* @author: gdg
* @Date: 2023/7/13
*/
public class MyPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
List partitions = cluster.partitionsForTopic(topic);
//分区数
int numPartitions = partitions.size();
// 根据业务逻辑,自定义消息的分区规则
// 在这个示例中,我们假设 key 是一个字符串类型,根据 key 的哈希值来决定分区
if (key instanceof String) {
String keyString = (String) key;
int partition = Math.abs(keyString.hashCode() % numPartitions);
return partition;
}
// 如果 key 不是字符串类型,则抛出异常
throw new InvalidRecordException("Invalid key type. It should be a string.");
}
@Override
public void close() {
// 在需要清理资源时执行的逻辑
}
@Override
public void configure(Map map) {
// 在需要配置一些参数时执行的逻辑
}
}
package com.example.kafkademo.config;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import java.util.HashMap;
import java.util.Map;
/**
* @description <功能描述>
* @author: gdg
* @Date: 2023/7/13
* 使用 @EnableKafka :
* 1 可以使用 @KafkaListener 注解来标注方法,指定要监听的主题、消费者组等配置,从而实现接收和处理 Kafka 消息的功能
* 2 会根据默认配置或者自定义配置进行初始化 从而简化了 Kafka 相关组件的配置和创建过程。
*
*/
@Configuration
@EnableKafka
public class KafkaProducerConfig {
@Value("${kafkaserver.server}")
private String boardServer;
public Map producerConfigs(){
Map props = new HashMap<>();
// 指定多个kafka集群多个地址
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, boardServer);
// 重试次数,0为不启用重试机制
props.put(ProducerConfig.RETRIES_CONFIG, 0);
//同步到副本, 默认为1
// acks=0 把消息发送到kafka就认为发送成功
// acks=1 把消息发送到kafka leader分区,并且写入磁盘就认为发送成功
// acks=all 把消息发送到kafka leader分区,并且leader分区的副本follower对消息进行了同步就任务发送成功
props.put(ProducerConfig.ACKS_CONFIG, "1");
// 生产者空间不足时,send()被阻塞的时间,默认60s
props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 6000);
// 控制批处理大小,单位为字节
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 4096);
// 批量发送,延迟为1毫秒,启用该功能能有效减少生产者发送消息次数,从而提高并发量
props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
// 设置生产者发送缓冲区的大小的属性。 发送缓冲区:Kafka 生产者用来临时存储待发送消息的内存区域
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 40960);
// 消息的最大字节大小,也就是说send的消息大小不能超过这个限制, 默认1048576(1MB)
props.put(ProducerConfig.MAX_REQUEST_SIZE_CONFIG,1048576);
// 键的序列化方式
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// 值的序列化方式
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// 压缩消息,支持四种类型,分别为:none、lz4、gzip、snappy,默认为none。
// 消费者默认支持解压,所以压缩设置在生产者,消费者无需设置。
//none:不进行压缩,消息以原始格式发送。
//gzip:使用 GZIP 压缩算法进行压缩。
//snappy:使用 Snappy 压缩算法进行压缩。
//lz4:使用 LZ4 压缩算法进行压缩。
//需要注意的是,压缩会增加消息的处理开销,因为在发送和接收消息时需要进行压缩和解压缩操作。
props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"none");
//设置分区规则
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class);
return props;
}
/**
* Producer 工厂配置
* 可以自定义创建 Kafka 生产者
* 设置生产者的各种属性,如 Kafka 服务器地址、序列化器、分区器、拦截器等
*/
@Bean
public ProducerFactory producerFactory(){
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
/**
* Producer Template 配置
*/
@Bean(name="kafkaTemplate")
public KafkaTemplate kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}
package com.example.kafkademo.config;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.InvalidRecordException;
import org.apache.kafka.common.PartitionInfo;
import java.util.List;
import java.util.Map;
/**
* @description <自定义分区规则>
* @author: gdg
* @Date: 2023/7/13
*/
public class MyPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
List partitions = cluster.partitionsForTopic(topic);
//分区数
int numPartitions = partitions.size();
// 根据业务逻辑,自定义消息的分区规则
// 在这个示例中,我们假设 key 是一个字符串类型,根据 key 的哈希值来决定分区
if (key instanceof String) {
String keyString = (String) key;
int partition = Math.abs(keyString.hashCode() % numPartitions);
return partition;
}
// 如果 key 不是字符串类型,则抛出异常
throw new InvalidRecordException("Invalid key type. It should be a string.");
}
@Override
public void close() {
// 在需要清理资源时执行的逻辑
}
@Override
public void configure(Map map) {
// 在需要配置一些参数时执行的逻辑
}
}
配置同一个topic , 监听不同分区
package com.example.kafkademo.manager;
import com.alibaba.fastjson.JSONObject;
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.annotation.TopicPartition;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Optional;
/**
* @description <功能描述>
* @author: gdg
* @Date: 2023/7/13
*/
@Component
public class KafkaComsumerManager {
private static Logger logger = LoggerFactory.getLogger("adminLogger");
private static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 指定topic
* 指定消费分区parttition
* topicPartitions : 由主题名和分区号组成的列表或集合。它用于在消费者端进行分区级别的操作和处理
* containerFactory : 指定消息监听 用于消费主题中的消息
* ${kafkaserver.parttition} ${} 是一种属性占位符语法,用于引用配置文件中的属性值 会从 YAML 配置文件中获取对应的值。
*
* @param records
*/
@KafkaListener(topicPartitions = {
@TopicPartition(topic = "${kafkaserver.topic}",partitions = "${kafkaserver.parttition}" ),
@TopicPartition(topic = "${kafkaserver.topic}",partitions = "${kafkaserver.parttition1}" ),
},
containerFactory = "kafkaListenerContainerFactory",
groupId = "group-test-1")
public void onMessage(List records) {
logger.info("**********************************接收数量{}**************************************",records.size());
for(ConsumerRecord record :records ){
Optional
package com.example.kafkademo.produce;
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @description <功能描述>
* @author: gdg
* @Date: 2023/7/13
*/
@RestController
@RequestMapping("/pro")
public class ProducerController {
private static Logger logger = LoggerFactory.getLogger("adminLogger");
@Autowired
@Qualifier("kafkaTemplate")
private KafkaTemplate kafkaTemplate;
@Value("${kafkaserver.topic}")
private String topic;
/**
* producer 同步方式发送数据
* get()方法
* 1 会阻塞当前线程直到发送完成,并返回发送结果对象
* 2 可以实现同步地发送消息并等待发送完成
* get(10, TimeUnit.SECONDS) 设置10秒内发送完成否则将会抛出 TimeoutException 异常
*
* @param topic topic名称
* @param message producer发送的数据
*/
@PostMapping("/syncSend")
public void sendMessageSync()
throws InterruptedException, ExecutionException, TimeoutException {
for (int i = 0; i < 50; i++) {
Map map = new HashMap<>();
map.put("userId","1000"+i);
map.put("userName","阿萨德"+i);
String message = JSON.toJSONString(map);
kafkaTemplate.send(topic, message).get(10, TimeUnit.SECONDS);
}
}
@PostMapping(value = "/producerData")
public void producerData() {
for (int i = 0; i < 50; i++) {
Map map = new HashMap<>();
map.put("userId","1000"+i);
map.put("userName","阿萨德"+i);
String message = JSON.toJSONString(map);
sendMessageAsync(topic,generateRandomString(10),message);
}
}
private String generateRandomString(int length) {
SecureRandom secureRandom = new SecureRandom();
byte[] randomBytes = new byte[length];
secureRandom.nextBytes(randomBytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
}
/**
* producer 异步方式发送数据
*
* @param topic topic名称
* @param message producer发送的数据
*/
private void sendMessageAsync(String topic,String key, String message) {
logger.info("准备发送消息为:{}", message);
//发送消息
CompletableFuture> future = kafkaTemplate.send(topic,key,message);
//处理发送后的操作
future.handle((result, ex) -> {
if (ex != null) {
// 在这里处理异常情况
logger.error("{} - 生产者 发送消息失败:{}", topic, ex.getMessage());
// 返回一个默认的 SendResult 或其他你希望返回的默认值
return null;
} else {
// 返回原始的 SendResult
logger.info("{} - 生产者 发送消息成功:{}" , topic, result.getProducerRecord().value());
return result;
}
});
}
}
cd /opt/kafka/bin/
# 创建topic名称为first,3个分区,1个副本
./kafka-topics.sh --zookeeper 192.168.16.131:12181 --create --topic first --replication-factor 1 --partitions 3
kafka-topics.sh --describe --zookeeper 192.168.16.131:12181 --topic dume-topic
---ReplicationFactor : 副本数
cd /opt/kafka_2.13-2.8.1/config/ 查询 server.properties文件,确定分区存储路径
cd /kafka/kafka-logs-d3c8611dfd21/
分区的平均分散是通过分区的分配算法来实现的。Kafka 使用一种称为“分区分配策略”的算法,根据一定的规则将分区均匀地分配给可用的 Broker
常用的分区分配策略有以下几种:
注意; 分区的分配和平衡是在 Kafka 集群中动态进行的,当集群的 Broker 增加或减少时,Kafka 会自动进行分区的重新分配,以适应集群的变化和维持平衡状态
当有新的消费者加入消费者组、已有的消费者退出消费者组或者订阅的主体分区发生了变化,会触发分区的重新分配操作,重新分配的过程称为Rebalance