Kafka面试常客
消费组
用来组织管理消费者一个特性,同组负载均衡,不同组广播
同组负载均衡
相同的消费组的消费者实例运行多个
// 运行多个消费者服务实例
prop.put(ConsumerConfig.GROUP_ID_CONFIG, "g1"); // 消费组 不同组广播 同组负载均衡
结论:
- 同组 负载均衡,同组一个消费者负责一个或多个分区的数据处理,分区到消费者是一个平行关系(正常情况下)
- 当同组的某些消费者故障时,故障消费者所负责处理的分区数据,会自动进行容错处理(将故障消费者所负责的分区,交给其余可用的消费者消费处理)
不同组广播
多个消费者属于不同的消费组
prop.put(ConsumerConfig.GROUP_ID_CONFIG, "g2"); // 消费组 不同组广播 同组负载均衡
结论:
- 不同的消费组,Topic数据会复制给所有的消费组,但是只能由消费组中的一个消费者进行数据的处理
生产者记录发布策略
Kafka生产者用以产生数据,并且将数据发送到kafka集群进行持久化存储; 发布策略有如下几种:
- Record的Key不为空,则使用哈希取模的发布策略(key.hashCode % numPartitions = 分区序号)
- Record的Key为空,则使用轮询分区的发布策略
- 手动指定Record存储的分区序号
// 测试生产者发布策略
// key != null
/*
ProducerRecord<String, String> record1 = new ProducerRecord<String, String>("t2", "user006", "xh1");
ProducerRecord<String, String> record2 = new ProducerRecord<String, String>("t2", "user006", "xh2");
ProducerRecord<String, String> record3 = new ProducerRecord<String, String>("t2", "user006", "xh3");
producer.send(record1);
producer.send(record2);
producer.send(record3);
*/
// key == null
/*
ProducerRecord<String, String> record1 = new ProducerRecord<String, String>("t2", "xh1");
ProducerRecord<String, String> record2 = new ProducerRecord<String, String>("t2", "xh2");
ProducerRecord<String, String> record3 = new ProducerRecord<String, String>("t2", "xh3");
producer.send(record1);
producer.send(record2);
producer.send(record3);
*/
// 手动指定分区序号 p0分区
ProducerRecord<String, String> record1 = new ProducerRecord<String, String>("t2", 0, "user007", "xh4");
producer.send(record1);
ProducerRecord<String, String> record2 = new ProducerRecord<String, String>("t2", 2, "user007", "xh5");
producer.send(record2);
消费者消费方式
Kafka消费者订阅1个或者多个感兴趣Kafka Topic,当这些Topic有新的数据产生,消费者拉取最新的数据,然后进行相应的业务处理;
有三种消费方式:
- 只订阅(subscribe): 订阅1到N个Topic的所有分区
- 指定消费分区: 订阅某一个Topic的特定分区
- 手动指定分区消费位置: 每一个消费者维护一个消费信息(元数据,读位置offset),可以手动重置offset;这样做的目的可以重新消费已经处理过的数据或者跳过不感兴趣的数据
// 订阅主题
// consumer.subscribe(Arrays.asList("t2"));
// 指定消费分区 只处理t2 topic p0分区数据
// consumer.assign(Arrays.asList(new TopicPartition("t2",0)));
// 手动指定消费位置
consumer.assign(Arrays.asList(new TopicPartition("t2",0)));
consumer.seek(new TopicPartition("t2",0),31);
首次订阅 offset重置方式
Kafka消费者在第一次(首次)订阅某个Topic时,offset默认采用的重置方式为latest
(默认), 还有另外的一个方式earliest
;
自动重置消费位置 auto.offset.reset = latest
,结论:
- latest: 如果当前分区有已提交的offset,从已提交的offset之后消费数据;如果没有提交的offset,则从最后(最新产生的数据)消费数据
- earliest:如果当前分区有已提交的offset,从已提交的offset之后消费数据;如果没有提交的offset,则从分区的最前(开头)消费数据
结论:
- kafka消费位置基于消费组管理,并且kafka使用一个特殊Topic(
__consumer_offsets
),用以记录消费组对不同topic的消费位置。__consumer_offsets
由50个主分区构成,复制因子1,是一个系统topic
// latest
// prop.put(ConsumerConfig.GROUP_ID_CONFIG, "g1"); // 消费组 不同组广播 同组负载均衡
// prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest");
// earliest
prop.put(ConsumerConfig.GROUP_ID_CONFIG, "g2"); // 消费组 不同组广播 同组负载均衡
prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
消费者偏移量控制
Kafka消费者的消费位置有两种提交方法,一种自动提交(默认)和另一种手动提交(非默认)
将当前消费组中消费者一个消费位置offset,提交保存到__consumer_offsets
自动提交
# 每隔5秒将当前消费组中消费者的消费位置写入保存到__consumer_offsets
enable.auto.commit = true
auto.commit.interval.ms = 5000
手动提交
保证业务处理正常情况下提交读offset,非正常情况下不提交读offset,再下次拉取数据时依然会获取未正确处理的数据;
enable.auto.commit = false
prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// 业务处理完成后再手动提交消费位置
consumer.commitAsync();
自定义对象类型传输
- 生产者发布数据时: 自定义对象类型 implements Serializer
- 消费者消费数据时: 自定义对象类型 implements Deserializer
- 序列化策略:JSON、JDK、框架等
常用的序列化手段:bytes、json等
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
生产者的批处理
Kafka生产者生成的记录Record,首先进行缓存,然后定期或者在缓存空间即满时,一次性将多条数据写入到kafka集群;
注意:
批处理操作是一种惯用的kafka优化写方法,对于资源的利用率更高,但是有一定数据延迟;
batch.size = 4096
linger.ms = 5000
// 4096 = 4kb 设定批处理操作缓存区大小
prop.put(ProducerConfig.BATCH_SIZE_CONFIG,4096);
// 设定批处理操作 每一个批次逗留时间
prop.put(ProducerConfig.LINGER_MS_CONFIG,2000);
// 两个条件满足其一即可
Ack & Retries(应答&重试)机制
Kafka为了确保数据能够正确的写入到Kafka集群,提供了应答机制(ack);
Ack应答策略:
- ack = 0 无需应答 ack = 1 表示数据写入到主分区立即应答 (默认)
- ack = all 或者 -1
- 表示数据写入到主分区并且同步到复制分区后再进行应答
因为Kakfa Ack机制存在,当生产者发布的一个数据在写入Kafka集群时
如果长时间未获得ack应答,进行retry重试操作,(默认重试次数是Integer.MAX_VALUE)
// 生产者默认配置
acks = 1 #数据写入到主分区立即应答
request.timeout.ms = 30000 #应答超时时间30秒
retries = 2147483647 #应答重试次数Integer.MAX_VALUE(无限重试)
// 生产者默认配置
acks = 1
request.timeout.ms = 30000
retries = 2147483647
prop.put(ProducerConfig.ACKS_CONFIG, "all");
// 主分区和复制分区都写入后ack
prop.put(ProducerConfig.RETRIES_CONFIG, 10);
// 重试次数
prop.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 1);
// 请求超时时间 1ms 为了模拟
注意: 因为Kafka Retry机制存在有可能会导致Kafka集群存放多个相同数据;
如果要确保相同数据只保留一个,则需要开启Kafka幂等写操作;
幂等写操作
幂等: 一次操作和多次操作影响的结果是一致的;
//开启幂等写
prop.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
SpringBoot集成Kafka
- 创建SpringBoot
- 添加kafka配置
#====================== kafka =========================
spring.kafka.bootstrap-servers=node01:9092,node01:9092,node01:9092
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.acks=all
spring.kafka.producer.retries=10
spring.kafka.producer.batch-size=4096
spring.kafka.consumer.group-id=g1
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
生产者Demo
@SpringBootTest
class KafkaSbApplicationTests {
/**
* 生产者demo
*/
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Test
void test1() {
// 异步处理的结果对象
ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send("t3", "user00210", "xh210");
//回调函数
future.addCallback(
(stringStringSendResult) -> {
System.out.println("发送成功!");
},
(t) -> {
System.out.println("发送失败!");
t.printStackTrace();
}
);
}
}
消费者Demo
@KafkaListener(topics = "t3", groupId = "g1")
public void receive(ConsumerRecord<String, String> record) {
System.out.println(
record.key()
+ "\t"
+ record.value()
+ "\t"
+ record.timestamp()
+ "\t"
+ record.offset()
+ "\t"
+ record.partition()
+ "\t"
+ record.topic()
);
}
Kafka事务
什么是事务?
事务指的一个连贯的操作(不可分割整体),要么同时成功,要么同时失败;
Kafka事务类似于DB事务,隔离级别只有两种:
- read_uncommitted (默认) 读未提交 毫无意义
- read_committed 读已提交,解决脏读
使用:调用下面一个方法即可
注意:
事务操作 需要开启幂等写操作支持
事务操作 需要开启幂等写操作支持
// 初始化事务
producer.initTransactions()
// 开启事务
producer.beginTransaction()
// 提交事务
producer.commitTransaction()
// 取消事务
producer.abortTransaction()
// 发送事务偏移量信息
producer.sendOffsetsToTransaction