1 安装
2 启动及创建topic(windows)
启动zookeeper:
zookeeper-server-start.bat ..\..\config\zookeeper.properties
创建两个broker:
kafka-server-start.bat ..\..\config\server.properties
kafka-server-start.bat ..\..\config\server1.properties
创建一个topic,两个分区,每个分区两个副本:
kafka-topics.bat --create --bootstrap-server localhost:9092 --replication-factor 2 --partitions 2 --topic test2
查看主题:
kafka-topics.bat --list --bootstrap-server localhost:9092
创建一个生产者:
kafka-console-producer.bat --broker-list localhost:9092 --topic test2
创建一个消费者:
kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test2 --from-beginning
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LMDAufWv-1657845759222)(http://image.huawei.com/tiny-lts/v1/images/af18d613fbf90c02c52875cea106ab0c_1227x495.png)]
3 生产者
引入maven依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.0.0</version>
</dependency>
3.1 生产者的基本使用
下面的示例演示生产者的基本使用,发送为异步发送
@Test
void customProducer() {
Properties properties = new Properties();
// 1. 给 kafka 配置对象添加配置信息:bootstrap.servers
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
"localhost:9092,localhost:9093");
// 2. key,value 序列化(必须):key.serializer,value.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getCanonicalName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getCanonicalName());
// 3. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
// 4. 调用 send 方法,发送消息
for (int i = 0; i < 5; i++) {
kafkaProducer.send(new
ProducerRecord<>("test2","ceshi " + i));
}
// 5. 关闭资源
kafkaProducer.close();
}
执行结果:消费者收到消息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2iOzF3S0-1657845759223)(http://image.huawei.com/tiny-lts/v1/images/b6898547b2ca7e747fc45041a6f9e2ae_223x131.png)]
3.2 带回调函数的 异步发送
@Test
void customProducer2() throws Exception {
Properties properties = new Properties();
// 1. 给 kafka 配置对象添加配置信息:bootstrap.servers
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
"localhost:9092,localhost:9093");
// 2. key,value 序列化(必须):key.serializer,value.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getCanonicalName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getCanonicalName());
// 设置自定义的分区器
// properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.atgui gu.kafka.producer.MyPartitioner");
// 设置ack的模式,0表示生产者不等待服务器响应;1表示leader落盘后响应;
// all或者-1表示等待所有的isr(in-sync replicas)同步副本都落盘后响应,默认为all
properties.put(ProducerConfig.ACKS_CONFIG, "all");
// 3. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
// 4. 调用 send 方法,发送消息
for (int i = 0; i < 5; i++) {
// (1)send返回Future对象,如果想要改成同步调用,对send的返回结果调用get()即可
kafkaProducer.send(new ProducerRecord<>("test2","key" + i, "sishen " + i), new Callback() {
// 收到ack时调用
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
// 返回的信息包括主题、分区等
// 发送的信息默认分区策略,ProducerRecord如果
// (1)指定partition时,按照指定分区发送
// (2)没有指定partition,指定key时,按照key的hash值求余topic的分区数选择partition
// (3)没有指定partition和key:随机选择分区,并且采用粘性分区(安装发送批次,不是每个都随机)
// 发送分区的默认策略参考类 DefaultPartitioner 实现
System.out.println(" 主 题 : " +
metadata.topic() + "->" + "分区:" + metadata.partition());
} else {
// 出现异常打印
exception.printStackTrace();
}
}
});
// 延迟一会会看到数据发往不同分区
Thread.sleep(2);
}
// 5. 关闭资源
kafkaProducer.close();
}
3.3 自定义分区器
想要实现之定义分区器,需要实现Partitioner接口,并在创建生产者对象时,设置ProducerConfig.PARTITIONER_CLASS_CONFIG
属性
Partitioner接口定义:
public interface Partitioner extends Configurable, Closeable {
// 返回分区编号,可以根据key、value等相关特性,发送到指定分区
int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
void close();
// 通知创建一个新的发送批次
default void onNewBatch(String topic, Cluster cluster, int prevPartition) {
}
}
3.4 影响吞吐量的相关参数
- batch.size:批次大小,默认 16K
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); - linger.ms:等待时间,默认 0
properties.put(ProducerConfig.LINGER_MS_CONFIG, 1); - RecordAccumulator:缓冲区大小,默认 32M:buffer.memory
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); - compression.type:压缩,默认 none,可配置值 gzip、snappy、lz4 和 zstd
properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, “snappy”);
3.5 数据去重
3.5.1 数据传递语义
- 至少一次(at least once):acks级别为-1 + 分区副本数大于等于2 + ISR中最小副本数量大于等于2, 可以保证数据不丢失,无法保证数据不重复
- 最多一次(at most once): acks级别设置为0;可以保证数据不重复,无法保证数据不丢失。
- 精确一次(exactly once):既不重复,也不丢失;依靠幂等性 + 至少一次保证。
3.5.2 幂等性
Producer 的幂等性指的是当发送同一条消息时,数据在 Server 端只会被持久化一次,数据不丟不重,但是这里的幂等性是有条件的:
- 只能保证 Producer 在单个会话内不丟不重,如果 Producer 出现意外挂掉再重启是无法保证的(幂等性情况下,是无法获取之前的状态信息,因此是无法做到跨会话级别的不丢不重);
- 幂等性不能跨多个 Topic-Partition,只能保证单个 partition 内的幂等性,当涉及多个 Topic-Partition 时,这中间的状态并没有同步。
重复数据的判断标准:PID+partition+seqNum; 每个Producer 在初始化时都会被分配一个唯一的 PID,这个 PID 对应用是透明的,完全没有暴露给用户。对于一个给定的 PID,sequence number 将会从0开始自增,每个 Topic-Partition 都会有一个独立的 sequence number。
幂等性开启参数 enable.idempotence 默认为 true,false关闭。
3.5.4 事务
Kafka事务性主要是为了解决幂等性无法跨Partition运作的问题,事务性提供了多个Partition写入的原子性,即写入多个Partition要么全部成功,要么全部失败,不会出现部分成功部分失败这种情况。
@Test
void customProducerTransation() throws Exception {
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
"localhost:9092,localhost:9093");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getCanonicalName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getCanonicalName());
// 1.设置事务 id(必须),名字任意,但要唯一
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transaction_id_0");
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
// 2. 初始化事务
kafkaProducer.initTransactions();
// 3. 开启事务
kafkaProducer.beginTransaction();
try {
for (int i = 0; i < 5; i++) {
kafkaProducer.send(new ProducerRecord<>("test2", "key" + i, "transation " + i), new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
System.out.println(" 主 题 : " +
metadata.topic() + "->" + "分区:" + metadata.partition());
} else {
// 出现异常打印
exception.printStackTrace();
}
}
});
// 构造异常
// int ab = 1 / 0;
Thread.sleep(1);
}
// 4. 提交事务
kafkaProducer.commitTransaction();
} catch (Exception e) {
// 5. 终止事务
kafkaProducer.abortTransaction();
}
kafkaProducer.close();
}
3.6 有序性
- 多个分区间无法保证有序性
- 单个分区内有条件有序:(生产者发送消息到broker,有失败重试,可能会导致失序)
-
kafka在1.x版本之前保证数据单分区有序,条件如下:
max.in.flight.requests.per.connection=1(不需要考虑是否开启幂等性)
max.in.flight.requests.per.connection参数指定了生产者在收到服务器响应之前可以发送多少个消息。它的值越高,就会占用越多的内存,同时也会提升吞吐量。把它设为1就可以保证消息是按照发送的顺序写入服务器的 -
kafka在1.x及后续版本保证数据单分区有序,条件如下:
未开启幂等性:max.in.flight.request.per.conection需要设置为1
开启幂等性:max.in.flight.requests.per.connection需要设置小于等于5
说明:在kafka1.x以后,启用幂等性后,kafka服务端会缓存producer发来的最近5个request的元数据,故无论如何,都可以保证最近5个request的数据都是有序的
4 Broker
LEO(Log End Offset):每个副本的最后一个offset,LEO其实就是最新的offset + 1
HW(High Watermark):所有ISR中最小的LEO
4.1 follower故障处理
当follower出现故障时,由于leader并没有任何影响,整个集群仍然能够对外提供读写服务,内部大致的处理步骤如下:
- follower 发生故障后会被临时踢出ISR;
- 这个期间Leader和Follower继续接收数据;
- 待该follower恢复后,Follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向Leader进行同步;
- 等到该Follower的LEO大于等于该Partition的HW,即follower追上Leader之后,就可以重新加入ISR了
4.2 leader故障处理
leader处理故障的过程相对来说要简单一些,主要步骤如下:
- Leader发生故障之后,会从ISR中选出一个新的Leader;
- 为保证多个副本之间的数据一致性,其余的Follower会先将各自的log文件高于HW的部分截掉,然后从新的Leader同步数据。
也就是说,leader故障时,会优先保证数据的一致性,可能会出现数据丢失。
5 消费者
对于消费者,不是以单独的形式存在的,每一个消费者属于一个 consumer group,一个 group 包含多个 consumer。特别需要注意的是:订阅 Topic 是以一个消费组来订阅的,发送到 Topic 的消息,只会被订阅此 Topic 的每个group中的一个consumer消费。同一个消费组的两个消费者不会同时消费一个 partition。
@Test
void customConsumer() throws Exception {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
"localhost:9092,localhost:9093");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
// 配置消费者组(组名任意起名) 必须
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer1");
// 创建消费者对象
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer(properties);
// 注册要消费的主题(可以消费多个主题)
ArrayList<String> topics = new ArrayList<>();
topics.add("test2");
kafkaConsumer.subscribe(topics);
// 拉取数据打印
while (true) {
// 设置 1s 中消费一批数据
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
// 打印消费到的数据
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
}
5.1 提交模式
DMQ消费者从服务端收到消息后,需要告诉服务端收到了该消息,以便我们下次再去取的时候,知道从哪个位置(offset)开始
-
自动异步提交:
定时提交,定时器触发,由于每一次去DMQ获取消息,都是批量获取,再逐一回调业务的onMessage方法,因此如果业务处理慢,或者定时器间隔设置太短,都有可能造成,客户端内存消息还没有消费完,进程被杀,导致消息丢失(不是真正意义上的丢失,只是说没法再消费之前收下来未处理的消息)
优点:性能高,IO少
缺点:丢消息(概率低)
适合业务场景:比如日志处理,push消息回执等,并发要求高,消息可丢(一般是客户端kill重启的时候,有可能会丢,正常运行过程中不丢)
配置:
auto.commit.enable=true
auto.commit.interval.ms=60000 -
手动同步提交
DMQ的消息获取也是批量的,只是消费位置(offset)的提交,需要手动完成(我们的SDK做了),即业务的onMessage方法处理完成,保证成功后,才告诉服务端我收到,提交当前消息的offset
优点:消息不丢,且消息重复消费的概率、比例最低
缺点:性能差,每处理一条消息,就要提交一次offset,网络IO多,导致性能上不去
适合业务场景:用户订单消息,处理并发要求不高,但是消息不能丢,允许重复消费(一般是客户端kill重启的是,最后处理的消息还没有处理完,下次重启重复消费该条消息)
配置:
auto.commit.enable=fasle
dmq.manualCommit.async.enable=false -
手动异步提交(推荐)
与同步提交不同的时,提交Offset的动作是异步的,提交的offset会等到业务的onMessage方法处理完成,保证成功后才提交。
优点:消息不丢,性能比同步提交高很多,比异步提交差一点
缺点:重复消费的概率较高。(一般是客户端Kill重启,或者新加入客户端的时候,分区分配发生rebalance)
适合业务场景:数据同步,允许重复,不允许丢失,且对性能要求较高
配置:
auto.commit.enable=fasle
dmq.manualCommit.async.enable=true