1.ACK应答机制
producer将数据写入Kafka时,producer通过ACK应答机制确认数据是否到达Kafka,具体设置参数如下:
/* * 0 无需反馈 * 1 leader反馈 * -1 leader和所有follower反馈 */ prop.put(ProducerConfig.ACKS_CONFIG,"1");
2.ISR机制
当数据导入Brock后,leader会先让数据落盘,然后follow们会跟着落盘,这其中数据落盘会有快慢之分。当leader炸了时,follow要上去顶替leader,越快将数据落盘的follow里面的数据相较其他follow来说就越完整,这个快的follow就最好顶替leader,我们将快的follow标记进一个组,就是ISR组。ISR组里面可以有多少个follow也是可以通过修改文件设置的,理论来说ISR里面follow越多,数据落盘读取越安全
这又引出另一个问题,当我们设置ACK应答为-1,leader和ISR的follow都要确认数据才算落盘,如果ISR设置过大,那数据传输越慢,但是ISR设置太小数据可能会不安全。所谓鱼和熊掌不可兼得,数据传输速度和安全相互制约
3.producer用线程池进行数据提交
pom导入包:
<dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka_2.11</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-streams</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>org.apache.hbase</groupId> <artifactId>hbase-client</artifactId> <version>1.2.0</version> </dependency> <dependency> <groupId>org.apache.hbase</groupId> <artifactId>hbase-server</artifactId> <version>1.2.0</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.7.0</version> </dependency>
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.common.serialization.StringSerializer; import java.util.Properties; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class MyProducer3 { public static void main(String[] args) { //创建一个固定大小为5的线程池 ExecutorService executorService = Executors.newFixedThreadPool(5); //循环100次,提交100个任务 for(int i=0;i<100;i++){ executorService.submit(new Runnable() { @Override public void run() { //创建一个Properties对象,用于存储Kafka生产者的配置信息 Properties prop =new Properties(); //设置Kafka集群的地址 prop.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.52.146:9092"); //设置key的序列化类 prop.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); //设置value的序列化类 prop.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); //设置ack的值为1,表示只要有一个副本确认了消息就认为消息发送成功 prop.put(ProducerConfig.ACKS_CONFIG,"1"); //设置重试次数为3次 prop.put(ProducerConfig.RETRIES_CONFIG,"3"); //重试3次 //创建KafkaProducer对象 KafkaProducer<String, String> Producer = new KafkaProducer<>(prop); //循环100000次,发送100000条消息 //可以替换为外部消息推送 for (int j=0;j<100000;j++){ //生成消息 String msg = Thread.currentThread().getName()+" "+j; System.out.printf(msg); //创建ProducerRecord对象,指定topic和消息内容 ProducerRecord<String,String>record=new ProducerRecord<>("batchmsg",msg); //发送消息 Producer.send(record); } } }); } //停止接收新的任务 executorService.shutdown(); try { //等待所有任务执行完毕 executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); } catch (InterruptedException e) { e.printStackTrace(); } finally { } //输出“生成数据结束” System.out.printf("“生成数据结束"); } }
4.Kafka数据落盘
Kafka集群内部大概可以这样看
每个host可以创建几个broker,,集群里面不同broker创建相同的topic,一个broker里面可以创建几个topic(主题)相当于我们把消息的不同类别进行了分区,分区里面创建partition(分区),如p1,p2,p3,在p1里可能放的是a数据块的leader,p2里面放的是b数据块的follow等等,那host2的broker2里面的p1就是a数据块的follow,p2就是b数据块的leader等等,通过将不同的leader和follow数据块放在不同的broker里面,当其中一个broker炸了,另外的broker里面的follow可以接替leader,实现了数据的安全性
5.consumer自动偏移量(offset)重置策略
consumer在消费数据时,从Kafka集群拿取消息根据偏移量可以设置拿取的策略,设置代码如:
prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
"latest"
:表示当Kafka中没有找到消费者的偏移量时,消费者将从分区中最新的消息(即分区末尾)开始读取。这意味着消费者会错过所有在它开始消费之前生产的消息。
"none"
:如果找不到消费者组的偏移量,则抛出异常给消费者。
"earliest"
:表示当Kafka中没有找到消费者的偏移量时,或者偏移量不再有效时(比如因为数据已被删除),消费者将从分区中最早的消息开始读取。这意呀着消费者会错过在启动后到它开始消费之间生产的所有消息。
6.consumer消费消息代码
import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.common.serialization.StringDeserializer; import java.time.Duration; import java.util.Collections; import java.util.Properties; import static java.time.Duration.ofMillis; public class MyConsumer2 { public static void main(String[] args) { Properties prop = new Properties(); prop.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.52.146:9092"); prop.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class); prop.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false"); prop.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"500"); prop.put(ConsumerConfig.GROUP_ID_CONFIG,"MyGroup1"); prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest"); for (int i=0;i<3;i++){ //一个消费者组有多个消费者,同时topic主题也有多个分区,producer会给每个分区分配一个消费者 new Thread(new Runnable() { @Override public void run() { KafkaConsumer<String,String > consumer = new KafkaConsumer<>(prop); consumer.subscribe(Collections.singletonList("news")); while (true){ ConsumerRecords<String,String>records=consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<String,String> record:records) { long offset = record.offset(); //获取消息的偏移量 String topic = record.topic(); //获取消息的主题名 int partition = record.partition(); //获取消息的分区号 String value = record.value(); //获取消息的value值 String key = record.key();//获取消息的key值 long timestamp = record.timestamp(); //获取消息的时间戳 //打印消息 System.out.printf("offset:%s,topic:%s,partition:%s,value:%s,key:%s,timestamp:%s%n", offset, topic, partition, value, key, timestamp); consumer.commitAsync(); }} } }).start(); } } }
7.consumer炸了后重新连接broker解决代码
当consumer炸了以后,同一个组的consumer会接替炸了的consumer消费信息,当炸了的consumer恢复正常后,consumer如何重新连接到broker继续消费信息,因为要考虑到consumer炸了之后其他consumer还会消费炸了的consumer的消息,当炸了的恢复后,要将它连接到当前consumer消费到的位置,使用seek方法将redis存储到的位置根据偏移量进行纠正,代码如下(我将Kafka连接到redis存储数据)
import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.serialization.StringDeserializer; import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Properties; import java.util.Set; public class MyConsumer3 { public static void main(String[] args) { RedisJDBC redisJDBC = new RedisJDBC(); final String GroupId = "MyGroup1"; Properties prop = new Properties(); prop.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.52.146:9092"); prop.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); prop.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false"); prop.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"500"); prop.put(ConsumerConfig.GROUP_ID_CONFIG,"MyGroup1"); prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(prop);创建KafkaConsumer对象 consumer.subscribe(Collections.singleton("news")); //订阅主题 Set<TopicPartition> assignments =null;//Set是一个不包含重复元素的集合 while(assignments==null||assignments.size()==0) { //让Kafka消费者不断轮询(poll)主题,以获取新的消息,轮询间隔为100毫秒。 consumer.poll(Duration.ofMillis(100)); assignments=consumer.assignment(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } finally { } List<Devop> devops = RedisJDBC.getDevopByGroupName("news", GroupId);//获取consumer在redis上获取的消息数据位置 for (TopicPartition topicPartition:assignments){ if (devops.isEmpty()||devops == null){ break; } int tpNum=topicPartition.partition(); for (Devop devop:devops){ if(tpNum==devop.getPartition()){ consumer.seek(new TopicPartition(devop.getTopic(),devop.getPartition()),devop.getOffset()); } } } } while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<String, String> record : records) { long offset = record.offset(); //获取消息的偏移量 String topic = record.topic(); //获取消息的主题名 int partition = record.partition(); //获取消息的分区号 redisJDBC.Save(new Devop(topic,GroupId,partition,offset)); String value = record.value(); //获取消息的value值 String key = record.key();//获取消息的key值 long timestamp = record.timestamp(); //获取消息的时间戳 //打印消息 System.out.printf(Thread.currentThread().getName()+"===="+topic + "====" + offset + "====" + partition + "=====" + value + "====" + key + "====" + record.timestamp()); consumer.commitAsync(); } } } }