Kafka使用&原理

一、提前准备

1、三台服务器
2、安装好zookeeper;
3、安装好kafka( tar -zxvf kafka_2.11-0.11.0.0.tgz -soft/)
4、修改配置文件

#broker 的全局唯一编号,不能重复哦
broker.id=0
#删除 topic 功能使能
delete.topic.enable=true
#处理网络请求的线程数量
num.network.threads=3
#用来处理磁盘 IO 的现成数量
num.io.threads=8
#发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
#接收套接字的缓冲区大小
socket.receive.buffer.bytes=102400
#请求套接字的缓冲区大小
socket.request.max.bytes=104857600
#kafka 运行日志存放的路径
log.dirs=          自    己    修     改
#topic 在当前 broker 上的分区个数
num.partitions=1
#用来恢复和清理 data 下数据的线程数量
num.recovery.threads.per.data.dir=1
#segment 文件保留的最长时间,超时将被删除
log.retention.hours=168
#配置连接 Zookeeper 集群地址
zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181

二、操作

1、Linux命令行操作
①启动&关闭
bin/kafka-server-start.sh -daemon config/server.properties
bin/kafka-server-stop.sh stop
②查看当前服务器中的所有 topic
bin/kafka-topics.sh --zookeeper hadoop102:2181 --list
③创建topic
bin/kafka-topics.sh --zookeeper hadoop102:2181 --create --replication- factor 3 --partitions 1 --topic first
zookepper集群地址、副本数、分区数、主题名
④发送消息
bin/kafka-console-producer.sh --broker-list hadoop102:9092 --topic first

上面的意思是在控制台启动生产者,然后就可以逐行发送消息了。注意连接的是broker-list
⑤消费消息
bin/kafka-console-consumer.sh --zookeeper hadoop102:2181 --topic first
或下面这个命令
bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first
这样就能接收消息了,如果想从头接加上–from-beginning

2、API操作
①导入依赖(生产和消费是同一个依赖)

<dependency>
	<groupId>org.apache.kafka</groupId>
	<artifactId>kafka-clients</artifactId>
	<version>0.11.0.0</version>
</dependency>

②生产数据

import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
	public static void main(String[] args) throws Exception {
		Properties props = new Properties();
		props.put("bootstrap.servers", "hadoop102:9092");//kafka 集群,broker-list
		props.put("acks", "all");
		props.put("retries", 1);//重试次数
		props.put("batch.size", 16384);//批次大小
		props.put("linger.ms", 1);//等待时间
		props.put("buffer.memory", 33554432);//RecordAccumulator 缓冲区大小
		props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
		props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
		//创建一个生产者对象
		Producer<String, String> producer = new KafkaProducer<>(props);
		for (int i = 0; i < 100; i++) {
			//发送数据,first主题、key是i、value也是i,最后有个回调函数
			producer.send(new ProducerRecord<String, String>("first", 
			Integer.toString(i), Integer.toString(i)), new Callback() {
			//回调函数,该方法会在 Producer 收到 ack 时调用,为异步调用
			@Override
			public void onCompletion(RecordMetadata metadata, Exception exception) {
				if (exception == null) {
					System.out.println("success->" + metadata.offset());
				} else {
					exception.printStackTrace();
				} }
			});
}
producer.close();
} }

③消费数据

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
public class CustomConsumer {
	public static void main(String[] args) {
		Properties props = new Properties();
		//Kafka 集群
		props.put("bootstrap.servers", "hadoop102:9092");
		//消费者组,只要 group.id 相同,就属于同一个消费者组
		props.put("group.id", "test");
		//关闭自动提交 offset
		props.put("enable.auto.commit", "false");
		props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
		props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
		KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
		consumer.subscribe(Arrays.asList("first"));//消费者订阅主题
		while (true) {
			ConsumerRecords<String, String> records = consumer.poll(100);//消费者拉取数据
			for (ConsumerRecord<String, String> record : records) {
				System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
			}
			//异步提交
			consumer.commitAsync(new OffsetCommitCallback() {
				@Override
				public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
					if (exception != null) {
						System.err.println("Commit failed for" + offsets);
					 } 
				}
			});
} } }

三、细节说明

1、原理图
在这里插入图片描述
①几个概念:生产者、消费者、消费者组、kafka集群、Broker、Zookeeper、偏移量、主题、分区、副本

2、存储机制
在这里插入图片描述
①数据存放位置
kafka目录data文件夹下有多个以主题-分区命名的文件夹,每个文件夹里面有以.index和.log文件结尾的文件,成对出现;
②log文件里面只放数据,index文件放的是索引,指向log文件。所以查找的时候快很多;
③先用二分查找找到是哪个index文件(因为index文件命名是以偏移量命名的),然后index文件里面存的每条数据大小一定,所以能很快定位到,然后就可以指向log文件啦。

四、生产者系列

0、发送流程
在这里插入图片描述
Kafka 的 Producer 发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程,以及一个线程共享变量——RecordAccumulator。
main 线程将消息发送给 RecordAccumulator,Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka broker。

1、将 producer 发送的数据封装成一个 ProducerRecord 对象。
在这里插入图片描述
①必须指定主题;
②如果指定分区,那么会到分区中,没有分区那么根据key的hash值与分区数取余,如果没有key那么就随机划分;

2、数据可靠性(ack)
①发送时机
等所有follower回复后才通知生产者;
②isr
如果等所有的follower都回复,那么有可能等很长时间,所以引入isr。将回复快的follower选入进去,将来leader也从其中选举。如果isr中的都回复了那么就向生产者返回ack
③ack是否启用
参数为0,那么不等ack继续发,会产生数据丢失;
参数为1,生产者等待ack,但是如果leader写完了就返回ack,有可能造成follower的数据丢失。
参数为-1,生产者等待ack,且isr中所有都写完后才返回ack,但是如果此时leader挂了,那么生产者还会再发一次数据,造成数据冗余。
④幂等性机制
开启幂等性的 Producer 在初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number。而Broker 端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时,Broker 只会持久化一条。
但是 PID 重启就会变化,同时不同的 Partition 也具有不同主键,所以幂等性无法保证跨分区跨会话的 Exactly Once。

3、副本一致性
这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
在这里插入图片描述
①HW之前的数据才对外可见,这样可以从根源上保证了彼此数据一致;
②如果leader挂了,上来的从机告诉其他人多退少补。

五消费者系列

1、offset保存
①可以自动提交或手动提交offset,但均有问题:先消费后提交会产生重复消费、先提交后消费则会漏掉数据;
②自定义存储offset
Kafka 0.9 版本之前,offset 存储在 zookeeper,0.9 版本及之后,默认将 offset 存储在 Kafka的一个内置的 topic 中。除此之外,Kafka 还可以选择自定义存储 offset。 offset 的维护是相当繁琐的,因为需要考虑到消费者的 Rebalace。
当有新的消费者加入消费者组、已有的消费者退出消费者组或者所订阅的主题的分区发生变化,就会触发到分区的重新分配,重新分配的过程叫做 Rebalance。
消费者发生 Rebalance 之后,每个消费者消费的分区就会发生变化。因此消费者要首先获取到自己被重新分配到的分区,并且定位到每个分区最近提交的 offset 位置继续消费。
要实现自定义存储 offset,需要借助 ConsumerRebalanceListener,以下为示例代码,其中提交和获取 offset 的方法,需要根据所选的 offset 存储系统自行实现。

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import java.util.*;
public class CustomConsumer {
	private static Map<TopicPartition, Long> currentOffset = new HashMap<>();
	public static void main(String[] args) {
		//创建配置信息
		Properties props = new Properties();
		//Kafka 集群
		props.put("bootstrap.servers", "hadoop102:9092");
		//消费者组,只要 group.id 相同,就属于同一个消费者组
		props.put("group.id", "test");
		//关闭自动提交 offset
		props.put("enable.auto.commit", "false");
		//Key 和 Value 的反序列化类
		props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
		props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
		//创建一个消费者
		KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
		//消费者订阅主题
		consumer.subscribe(Arrays.asList("first"), 
			new ConsumerRebalanceListener() {
				//该方法会在 Rebalance 之前调用
				@Override
				public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
					commitOffset(currentOffset);
				}
				//该方法会在 Rebalance 之后调用
				@Override
				public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
					currentOffset.clear();
					for (TopicPartition partition : partitions) {
						consumer.seek(partition,getOffset(partition));//定位到最近提交的 offset 位置继续消费
					} 
				}
			});
		while (true) {
			ConsumerRecords<String, String> records = consumer.poll(100);//消费者拉取数据
			for (ConsumerRecord<String, String> record : records) {
				System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
				currentOffset.put(new TopicPartition(record.topic(), record.partition()), record.offset());
			}
		commitOffset(currentOffset);//异步提交
		} 
	}

	//获取某分区的最新 offset
	private static long getOffset(TopicPartition partition) {
		return 0;
	}
	//提交该消费者所有分区的 offset
	private static void commitOffset(Map<TopicPartition, Long> 
		currentOffset) {
	}
 }

①在订阅的时候传入一个ConsumerRebalanceListener,在rebalance可以拉取和提交;
②之后每次消费都把目前位置结果记录到一个hashMap中,最后提交一下;
③可以把获取和提交的地方写一个JDBC,并加上事务,这样就可以保证消费者数据可靠性了。

六自定义拦截器

1、手写拦截器
一个类实现ProducerInterceptor接口即可

import java.util.Map;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public class TimeInterceptor implements ProducerInterceptor<String, String> {
	@Override
	public void configure(Map<String, ?> configs) {
	}
	@Override
	public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
		// 创建一个新的 record,把时间戳写入消息体的最前部
		return new ProducerRecord(record.topic(), record.partition(),record.timestamp(), record.key(),
			System.currentTimeMillis() + "," + record.value().toString());
	}
	@Override
	public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
		// RecordAccumulator 成功发送到 Kafka Broker 之后,或者在发送过程中失败时调用
		//所以可以进行发送成功/失败消息的计数
	}
	@Override
	public void close() {
	} 
}

2、生产者加入拦截器链

List<String> interceptors = new ArrayList<>();
interceptors.add("自定义拦截器包的全路径 cn.learn.interceptor.TimeInterce
ptor");
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值