先启动zookeeper
然后启动kafka
创建topic 在node02 节点
kafka-topics.sh --zookeeper node02:2181/kafka --create --topic xxxx --partitions 2 --replication-factor 2
package com.msb.zk;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@SpringBootTest
class ZkApplicationTests {
@Test
void contextLoads() {
}
/*
创建topic
* kafka-topics.sh --zookeeper node02:2181/kafka --create --topic xxxx --partitions 2 --replication-factor 2
*
*/
@Test
public void producer() throws ExecutionException, InterruptedException {
String topic="xxxx";
Properties properties = new Properties();
properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.1.137:9092,192.168.1.138:9092,192.168.1.139:9092" );
//kafka 持久化数据的MQ 数据->byte[] 不会对数据进行干预 双发要约定编解码
//kafka 是一个application 使用零拷贝 sendfile 系统调用实现快速数据消费
properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
//现在producer就是一个提供者 面向的其实是broker 虽然在使用的时候我们期望把数据打入topic
/**
* xxxx
* 2 partitions
* 2 replication
* 分区里面的消息是有序的 分区间的消息是无序的
* 一个生产者可以向多个分区发送数据
* 但是相同的key一定会达到一个分区中
*/
/**
* 三种商品 每种商品有线性的3个ID
* 相同的商品 最好去到一个分区里
*/
while (true){
for (int i = 0; i <3 ; i++) {//3商品
for (int j = 0; j <3 ; j++) {//3个id
ProducerRecord<String, String> record = new ProducerRecord<>(topic, "item"+j,"value" + i);
Future<RecordMetadata> send = producer.send(record);
RecordMetadata rm = send.get();
int partition = rm.partition();
long offset = rm.offset();
System.out.println("key:"+record.key()+" val:"+record.value()+" partitions:"+partition+" offset:"+offset);
}
}
}
}
@Test
public void consumer(){
//基础配置
Properties properties = new Properties();
properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.1.137:9092,192.168.1.138:9092,192.168.1.139:9092");
properties.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
/**
* 消费者细节 消费者从属于哪个组 一个组里有多少个consumer
* 一个组里的consumer可以消费一个分区 一个分区可以被一个组里的一个consumer消费 但是可以被不同分区的consumer 消费
*/
properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "xxxx03");
//KAFKA 是MQ 也是一个STORAGE
//What to do when there is no initial offset in Kafka or if the current offset does not exist any more on the server
// (e.g. because that data has been deleted):
// <ul><li>earliest: automatically reset the offset to the earliest offset<li>
// latest: automatically reset the offset to the latest offset</li><li>
// none: throw exception to the consumer if no previous offset is found for the consumer's group</li>
// <li>anything else: throw exception to the consumer.</li></ul>
//第一次启动没有offset
// properties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");//会追溯到历史消息最后 等最新的消息
properties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
/**开启自动提交 异步提交
* 上面说如果没有找到offset offset 是一个持久化单进程的 但刚起动是没有offset的
* 当offset拉过来了还没计算的 就提交了 就会丢失数据
*/
/*当properties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest")//此时是尽早的获取数据
properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false")//关闭自动提交 会造成数据的重复消费
当properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true")
自动提交间隔是properties.setProperty(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "15000"); 就算开启了自动提交 也会等到15秒以后自动提交
但是在这15秒以内 容易造成数据的丢失
一个运行的consumer,那么自己会维护自己的消费进度
一旦你自动提交,但是是异步的
1还没到时间,挂了 没提交 ,那么重启一个consumer 参照offset的时候 会重复消费
2一个批次的数据还没写数据库成功 但是这个批次的offset 异步提交了, 这个时候挂了,重启一个consumer 参照offset的时候 会丢失消息
[root@node02 node02]# kafka-consumer-groups.sh --bootstrap-server 192.168.1.137:9092 --describe --group xxxx01
Consumer group 'xxxx01' has no active members.
TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
xxxx 1 44255 51841 7586 - - -
xxxx 0 22127 25928 3801 - - -
这里的当前offset 只消费到44255 但是日志记录到51841 这里 下次会从日志加载 减去LAG 就是下次加载的位置 但是一旦宕机 数据丢失 会造成重复消费
* */
properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");//自动提交 异步提交 容易丢数据 重复数据
// properties.setProperty(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "15000");//默认自动提交间隔5秒
//properties.setProperty(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, );//kafka是发布订阅的 靠的是poll拉取数据 弹性 按需的 拉取多少
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
//先去订阅 topic,后面可以接参数 kafka 的consumer会动态负载均衡
consumer.subscribe(Arrays.asList("xxxx"), new ConsumerRebalanceListener() {
//消失那些分区
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
System.out.println("----------onPartitionsRevoked------------");
Iterator<TopicPartition> iterator = partitions.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
//得到哪些分区
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
System.out.println("----------onPartitionsAssigned------------");
Iterator<TopicPartition> iterator = partitions.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
});
while(true){
/*
如果想多线程处理多分区
每poll一次 用一个语义:一个job启动
一次job用多线程并行处理分区
且 job应该被控制是串行的 一个job没跑完 后面的job会堆积
* */
//拉取 微批
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(0));//超时时间 有了数据就立刻返回 0-n条
/**kafka-consumer-groups.sh --bootstrap-server 192.168.1.137:9092 --list
*[root@node02 node02]# kafka-consumer-groups.sh --bootstrap-server 192.168.1.137:9092 --describe --group xxxx01
*
* TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
* xxxx 0 12 12 0 consumer-1-08faeb7f-c474-4466-ab64-39c0e3ccf64b /192.168.1.4 consumer-1
* xxxx 1 24 24 0 consumer-1-08faeb7f-c474-4466-ab64-39c0e3ccf64b /192.168.1.4 consumer-1
*/
if(!records.isEmpty()) {
//以下代码的优化很重要
System.out.println("---------------"+records.count()+"---------------------");
Set<TopicPartition> partitions = records.partitions();//每次poll的时候是去取多个分区的数据
//分区内的数据时有序的
/*如果手动提交offset
* 1按消息进度同步提交
* 2按分区粒度同步提交
* 3按当前poll的批次同步提交
* */
for (TopicPartition partition : partitions) {
List<ConsumerRecord<String, String>> records1 = records.records(partition);
//在一个微批里 按分区取poll回来的数据
//线性按分区处理,还可以并行按分区处理 用多线程的方式
Iterator<ConsumerRecord<String, String>> iterator = records1.iterator();
while (iterator.hasNext()){
ConsumerRecord<String, String> next = iterator.next();
int partition1 = next.partition();
long offset = next.offset();
System.out.println("key:"+next.key()+" value:"+next.value()+" partition:"+next.partition()+" offset:"+next.offset());
TopicPartition TopicPartition = new TopicPartition("xxxx", partition1);
//(Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback)
OffsetAndMetadata OffsetAndMetadata = new OffsetAndMetadata(offset);
Map<TopicPartition,OffsetAndMetadata> map =new HashMap();
map.put(TopicPartition, OffsetAndMetadata);
consumer.commitAsync(map, new OffsetCommitCallback() {
@Override
public void onComplete(Map<org.apache.kafka.common.TopicPartition, org.apache.kafka.clients.consumer.OffsetAndMetadata> map, Exception e) {
}
});//这个是最安全的 每条记录级的更新 第一点
//单线程 多线程 都可以
long offset1 = records1.get(records1.size() - 1).offset();
OffsetAndMetadata pom = new OffsetAndMetadata(offset);
Map<TopicPartition,OffsetAndMetadata> map1 =new HashMap();
map1.put(TopicPartition, pom);
}
consumer.commitAsync();//按分区粒度提交 第二点
/*因为你都拿到分区了
拿到分区的数据集
期望的是对数据 整体加工
问 你怎么知道最后一条offset
* */
}
consumer.commitAsync();//这个是poll的批次提交offset 第三点
/* Iterator<ConsumerRecord<String, String>> iterator = records.iterator();
while (iterator.hasNext()){
//因为一个consumer 可以消费多个分区 但是一个分区只能给一个组里的一个consumer消费 所以在遍历的时候可以发现他们是来自不同分区的
ConsumerRecord<String, String> record = iterator.next();
int partition = record.partition();
long offset = record.offset();
System.out.println("key:"+record.key()+" value:"+record.value()+" partition:"+record.partition()+" offset:"+record.offset());
}
*/
}
}
}
}