探究Kafka原理-3.生产者消费者API原理解析

news2024/11/23 10:25:14
  • 👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家
  • 📕系列专栏:Spring源码、JUC源码、Kafka原理
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:源码溯源,一探究竟
  • 📝联系方式:nhs19990716,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

文章目录

  • API 开发:producer 生产者
    • 生产者 api 示例
    • 必要的参数配置
    • 发送消息
      • 发后即忘( fire-and-forget)
      • 同步发送(sync )
      • 异步发送(async )
  • API 开发:consumer 消费
    • subscribe 订阅主题
      • 消费者组再均衡分区分配策略
        • Range Strategy
        • Round-Robin Strateg
        • Sticky Strategy
        • Cooperative Sticky Strategy
      • 消费者组再均衡流程
        • GroupCoordinator 介绍
      • eager 协议再均衡步骤细节
        • 定位 Group Coordinator
        • 加入组 Join The Group
        • 组信息同步 SYNC Group
        • 心跳联系 HEART BEAT
        • 再均衡流程
    • assign 订阅主题
    • subscribe 与 assign 的区别
    • 取消订阅
    • 消息的消费模式
    • 指定位移消费
    • 自动提交消费者偏移量
    • 手动提交消费者偏移量(调用 kafka api)
    • 手动提交位移(时机的选择)
    • 消费者提交偏移量方式的总结

API 开发:producer 生产者

生产者 api 示例

一个正常的生产逻辑需要具备以下几个步骤
(1)配置生产者参数及创建相应的生产者实例
(2)构建待发送的消息
(3)发送消息
(4)关闭生产者实例

首先,引入 maven 依赖

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

采用默认分区方式将消息散列的发送到各个分区当中

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.Properties;
/*

kafka生产者api代码示例

*/
public class MyProducer {
	public static void main(String[] args) throws InterruptedException {
		Properties props = new Properties();
        
		//设置 kafka 集群的地址 必选
        props.put("bootstrap.servers", "doitedu01:9092,doitedu02:9092,doitedu03:9092");
        
        
        //ack 模式,取值有 0,1,-1(all) , all 是最慢但最安全的 消息发送,应答级别
        props.put("acks", "all");
        
        
        
        //序列化器 因为业务数据有各种类型的,但是kafka底层存储里面不可能有各种类型的,只能是序列化的字节,所以不管你要发什么东西给它,都要提供一个序列化器,帮你能够把key value序列化成二进制的字节
        // 因为kafka底层的存储是没有类型维护机制的,用户所发的所有数据类型,都必须 序列化成byte[],所以kafka的producer需要一个针对用户所发送的数据类型的序列化工具类,且这个序列化工具类,需要实现kafka所提供的序列工具接口。
        props.put("key.serializer",
        "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer",
        "org.apache.kafka.common.serialization.StringSerializer");
        
        
        
        /*
        需要额外的指定泛型,key value
        */
        Producer<String, String> producer = new KafkaProducer<>(props);
        for (int i = 0; i < 100; i++)
            // 其调用是异步的,数据的发送动作在producer的底层是异步线程的
        	producer.send(new ProducerRecord<String, String>("test",
        Integer.toString(i), "dd:"+i));
        // 在这里面可以通过逻辑判断去指定发送到那个topic中
        //Thread.sleep(100);
        producer.close();
	}
}

消息对象 ProducerRecord,除了包含业务数据外,还包含了多个属性:

public class ProducerRecord<K, V> {
    private final String topic;
    private final Integer partition;
    private final Headers headers;
    private final K key;
    private final V value;
    private final Long timestamp;

其发送方法中,根据参数的不同,有不同的构造方法

在这里插入图片描述

其实这样也就意味着我们可以把消息发送到不同的topic。

必要的参数配置

Kafka 生产者客户端 KakaProducer 中有 3 个参数是必填的。

bootstrap.servers / key.serializer / value.serializer

为了防止参数名字符串书写错误,可以使用如下方式进行设置:

pro.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
pro.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());

发送消息

创建生产者实例和构建消息之后 就可以开始发送消息了。发送消息主要有 3 种模式:

发后即忘( fire-and-forget)

发后即忘,它只管往 Kafka 发送,并不关心消息是否正确到达。
在大多数情况下,这种发送方式没有问题;
不过在某些时候(比如发生不可重试异常时)会造成消息的丢失。
这种发送方式的性能最高,可靠性最差。

Future<RecordMetadata> send = producer.send(rcd);

同步发送(sync )

try {
	producer.send(rcd).get();
} catch (Exception e) {
	e.printStackTrace();
}

因为Future的get方法是同步阻塞的。

异步发送(async )

回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是 RecordMetadata 和Exception,如果 Exception 为 null,说明消息发送成功,如果 Exception 不为 null,说明消息发送失败。

注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。

import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class MyProducer {
    public static void main(String[] args) throws InterruptedException {
        Properties props = new Properties();
		// Kafka 服务端的主机名和端口号
        props.put("bootstrap.servers", "doitedu01:9092,doitedu02:9092,doitedu03:9092");
		// 等待所有副本节点的应答
        props.put("acks", "all");
		// 消息发送最大尝试次数
        props.put("retries", 0);
		// 一批消息处理大小
        props.put("batch.size", 16384);
		// 增加服务端请求延时
        props.put("linger.ms", 1);
		// 发送缓存区内存大小
        props.put("buffer.memory", 33554432);
		// key 序列化
        props.put("key.serializer",
                "org.apache.kafka.common.serialization.StringSerializer");
		// value 序列化
        props.put("value.serializer",
                "org.apache.kafka.common.serialization.StringSerializer");
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);
        for (int i = 0; i < 50; i++) {
            kafkaProducer.send(new ProducerRecord<String, String>("test", "hello" + i),
                    new Callback() {
                        @Override
                        public void onCompletion(RecordMetadata metadata, Exception exception) {
                            if (metadata != null) {
                                System.out.println(metadata.partition()+ "-"+ metadata.offset());
                            }
                        }
                    });
        }
        kafkaProducer.close();
    }
}

API 开发:consumer 消费

import org.apache.kafka.clients.consumer.*;
import java.util.Arrays;
import java.util.Properties;
public class MyConsumer {
    public static void main(String[] args) {
        Properties props = new Properties();
		// 定义 kakfa 服务的地址,不需要将所有 broker 指定上
        // 客户端只要知道一台服务器,就能通过这一台服务器来获知整个集群的信息(所有的服务器、主机名等)
        // 如果你只填写一台,万一,你得客户端启动的时候,宕机了不在线,那就无法连接到集群了
        // 如果你填写了堕胎,有一个好处就是,万一连不上其中一个,可以去连接其它的
        props.put("bootstrap.servers", "doitedu01:9092");
        
		// 制定 consumer group
        props.put("group.id", "g1");
        
        
        // 按照一个时间间隔自动去提交偏移量
		// 是否自动提交 offset
        props.put("enable.auto.commit", "true");
		// 自动提交 offset 的时间间隔
        props.put("auto.commit.interval.ms", "1000");
        
        
		// key 的反序列化类
        props.put("key.deserializer",
                "org.apache.kafka.common.serialization.StringDeserializer");
		// value 的反序列化类
        props.put("value.deserializer",
                "org.apache.kafka.common.serialization.StringDeserializer");
        
        // kafka的消费者,默认是从属组之前所记录的偏移量开始消费,如果找不到之前记录的偏移量,则从如下参数配置的策略确定消费起始偏移量
		// 如果没有消费偏移量记录,则自动重设为起始 offset:latest, earliest, none
        /*
        earliest 自动重置到每个分区的最前一条消息
        latest   自动重置到每个分区的最新一条消息
        none	 没有重置策略
        */
        props.put("auto.offset.reset","earliest");
        
        
		// 定义 consumer
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        
        
        
		// 消费者订阅的 topic, 可同时订阅多个
        // subscribe订阅,是需要参与消费者组的再均衡机制才能真正获得自己要消费的topic及其分区
        // 只要消费者组里的消费者 变化了 就要发生再均衡
        consumer.subscribe(Arrays.asList("first", "test","test1"));
        
        
        
        
        // 显式指定消费起始偏移量(如果同时设置了消费者 偏移策略的话,以手动指定的为准)
        // 在设置消费分区起始偏移量这里,存在一个点,如果此时到这里了然后消费者组再均衡机制还没有做完,那么就会报错,因为可能这个消费者还没有被分配到这个分区  针对这个问题,其实动态再分配是有一个过程 和 时间的,谁也不知道要等多久,所以最好想的sleep就不容易实现了。
        想要解决这个问题有两种办法
            1.在这个过程中 拉一次数据,能拉到就代表再均衡机制完成了 consumer.poll(Long.MAX_VALVE);这里是无意义的拉一次数据,主要是为了确保分区分配已完成,然后就能够去定位偏移量了。但是这种方式不符合最初的设计初衷,如果是使用subscribe来订阅主题,那就意味着是应该参与这个组的均衡的,参与了,那就不要去指定组的偏移量了,应该听从组的分配。
            2.既然要自己指定一个确定的起始消费位置,那通常隐含之意就是不需要去参与消费者组的自动再均衡机制那么就不要使用subscribe来订阅主题
            consumer.assign(Arrays.asList(new TopicPartition("ddd",0))) 使用这个是不参与消费者的自动再均衡的。
        
        
        
        //TopicPartition first0 = new TopicPartition("first",0);
        //TopicPartition first1 = new TopicPartition("first",1);
        //consumer.seek(first0,10);
        //consumer.seek(first1,15);
        
        
        /*
        kafka消费者的起始消费位置有两种决定机制
        1.手动指定了起始位置,它肯定从你指定的位置开始
        2.如果没有手动指定位置,它会在找消费组之前所记录的偏移量开始
        3.如果之前的位置也获取不到,就看参数 : auto.offset.reset 所指定的重置策略
        */
        
        
        while (true) {
		// 读取数据,读取超时时间为 100ms
            ConsumerRecords<String, String> records = consumer.poll(100);
            for (ConsumerRecord<String, String> record : records)
                
                // ConsumerRecord中,不光有用户的业务数据,还有kafka塞入的元数据
                String key = record.key();
            	String value = record.value();
                
                // 本条数据所属的topic
            	String topic = record.topic();
            	// 本条数据所属的分区
            	int partition = record.partition
                
                // 本条数据的offset
                long offset = record.offset();
                
                // 当前这条数据所在分区的leader的朝代纪年
            	Optional<Integer> leaderEpoch = record.leaderEpoch();
            
            	// 在kafka的数据底层存储中,不光有用户的业务数据,还有大量元数据,timestamp就是其中之一:记录本条数据的时间戳,但是时间戳有两种类型,本条数据的创建时间(生产者)、本条数据的追加时间(broker写入log文件的时间)
                TimestampType timestampType = record.timestampType();
            	long timestamp = record.timestamp();
                
                // 数据头,是生产者在写入数据时附加进去的(相当于用户自己的元数据)
            	// 在生产者发送数据的时候,有一个构造方法可以允许你自己携带自己的 headers
            	Headers headers = record.headers();
                
                
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(),
                        record.key(), record.value());
        }
    }
}

如果消息还没生产到指定的位置呢?这是一个很有趣的问题,到底是等,还是报错

kafka-console-consumer.sh --bootstrap-server doit01:9092 --topic test --offset 100000 --partition 0  

假设分区0 中并没有offset >= 100000 的消息,执行之后,并不会报错,但是如果超标了,就会自动重置到最新的(lastest)。

如果如果指定的offset大于最大可用的offset,那么就会定义到最后一条消息。


subscribe 订阅主题

subscribe 有如下重载方法:

public void subscribe(Collection<String> topics,ConsumerRebalanceListener listener)
public void subscribe(Collection<String> topics)
public void subscribe(Pattern pattern, ConsumerRebalanceListener listener)
public void subscribe(Pattern pattern)

通过这几个构造函数来看,其中有ConsumerRebalanceListener listener 其实就是 再均衡 的监听器,再均衡的过程中,会调用这个方法。

Properties props = new Properties();
// 从配置文件中加载写好的参数
props.load(Consumer.class.getClassLoader.getResourceAsStream("consumer.properties"));
// 手动set一些参数进去
props.setProperty();
......
    
KafkaConsumer<String,String> consumer = new KafkaConsumer<>(props);


// reb-1 主题 3个分区
// reb-2 主题 2个分区
consumer.subscribe(Arrays.asList("reb-1","reb-2"),new ConsumerRebalanceListener(){
    // 再均衡分配过程中,消费者会取消先前所分配的主题、分区
    // 取消了之后,consumer会调用下面的方法
    public void onPartitionsRevoked(Collection<TopicPartition> partitions){
        
    }
    
    // 再均衡过程中,消费者会重新分配到新的主题、分区
    // 分配了新的主题 和 分区之后,consumer底层会调用下面的方法
    public void onPartitionAssigned(Collection<TopicPartition> partitions){
        
    }
});
但是以上的过程 懒加载,只有消费者真正 开始 poll的时候,才会实现再均衡分配的过程。

在这里插入图片描述

现有的再均衡原则就是每次有消费者增减 都会重新分配,其实就是先全部取消,然后又重新分配了呢,这过程中肯定存在消耗,得先把工作暂停,把偏移量记好,另外一个人接手的时候,还需要另外去读偏移量,重新从对应的位置开始。

而在kafka2.4.1中解决了这个重分配的问题。但是大多数使用的框架没有到这个版本,或者所使用的如spark flink等底层所依赖的kafka没有2.4.1这个版本。

消费者组再均衡分区分配策略

消费者组的意义何在?为了提高数据处理的并行度!

在这里插入图片描述

会触发 rebalance 的事件可能是如下任意一种:

  • 有新的消费者加入消费组。
  • 有消费者宕机下线,消费者并不一定需要真正下线,例如遇到长时间的 GC 、网络延迟导致消费者长时间未向 GroupCoordinator 发送心跳等情况时,GroupCoordinator 会认为消费者己下线。
  • 有消费者主动退出消费组(发送 LeaveGroupRequest 请求):比如客户端调用了 unsubscrible()方法取消对某些主题的订阅。
  • 消费组所对应的 GroupCoorinator 节点发生了变更。
  • 消费组内所订阅的任一主题或者主题的分区数量发生变化。

将分区的消费权从一个消费者移到另一个消费者称为再均衡(rebalance),如何 rebalance 也涉及到分区分配策略。

kafka 有两种的分区分配策略:range(默认) 和 round robin(新版本中又新增了另外 2 种)

我们可以通过 partition.assignment.strategy 参数选择 range 或 roundrobin。
partition.assignment.strategy 参数默认的值是 range。
partition.assignment.strategy=org.apache.kafka.clients.consumer.RoundRobinAssignor
partition.assignment.strategy=org.apache.kafka.clients.consumer.RangeAssignor

这个参数属于“消费者”参数!

Range Strategy
  • 先将消费者按照 client.id 字典排序,然后按 topic 逐个处理;
  • 针对一个 topic,将其 partition 总数/消费者数得到 商 n 和 余数 m,则每个 consumer 至少分到 n个分区,且前 m 个 consumer 每人多分一个分区;

举例说明 2:假设有 TOPIC_A 有 5 个分区,由 3 个 consumer(C1,C2,C3)来消费;

5/3 得到商 1,余 2,则每个消费者至少分 1 个分区,前两个消费者各多 1 个分区 C1: 2 个分区,C2:2 个分区, C3:1 个分区

接下来,就按照“区间”进行分配:

C1: TOPIC_A-0 TOPIC_A-1
C2: TOPIC_A-2 TOPIC_A_3
C3: TOPIC_A-4

举例说明 2:假设 TOPIC_A 有 5 个分区,TOPIC_B 有 3 个分区,由 2 个 consumer(C1,C2)来消费

先分配 TOPIC_A:

5/2 得到商 2,余 1,则 C1 有 3 个分区,C2 有 2 个分区,得到结果

C1: TOPIC_A-0 TOPIC_A-1 TOPIC_A-2
C2: TOPIC_A-3 TOPIC_A-4

再分配 TOPIC_B:

3/2 得到商 1,余 1,则 C1 有 2 个分区,C2 有 1 个分区,得到结果

C1: TOPIC_B-0 TOPIC_B-1
C2: TOPIC_B-2

最终分配结果:

C1: TOPIC_A-0 TOPIC_A-1 TOPIC_A-2 TOPIC_B-0 TOPIC_B-1
C2: TOPIC_A-3 TOPIC_A-4 TOPIC_B-2

如果共同订阅的主题很多,那也就意味着,排在前面的消费者拿到的分区会明显多余排在后面的。

而消费者本身有一个id,是根据id号去排序

以上就是该种模式的弊端,其实就是一个topic一个topic去分的。这个问题尤其是在订阅多个topic的时候最明显,分配单个topic的情况,也就多一个分区。

Round-Robin Strateg

将所有主题分区组成 TopicAndPartition 列表,并对 TopicAndPartition 列表按照其 hashCode 排序,然后,以轮询的方式分配给各消费者。

以上述问题来举例:

先对 TopicPartition 的 hashCode 排序,假如排序结果如下:

TOPIC_A-0 TOPIC_B-0 TOPIC_A-1 TOPIC_A-2 TOPIC_B-1 TOPIC_A-3 TOPIC_A-4 TOPIC_B-

然后按轮询方式分配

C1: TOPIC_A-0 TOPIC_A-1 TOPIC_B-1 TOPIC_A-4

C2: TOPIC_B-0 TOPIC_A-2 TOPIC_A-3 TOPIC_B-2

Sticky Strategy

对应的类叫做: org.apache.kafka.clients.consumer.StickyAssignor

sticky 策略的特点:

  • 要去打成最大化的均衡
  • 尽可能保留各消费者原来分配的分区

再均衡的过程中,还是会让各消费者先取消自身的分区,然后再重新分配(只不过是分配过程中会尽量让原来属于谁的分区依然分配给谁)

以一个例子来看

---开始
C1:A-P0		B-P1	B-P2
C2:B-P0		A-P1

---加入C3后再分配
Range Strategy
C1:A-P0		A-P1
C2:B-P0		B-P2
C3:B-P1


Sticky Strategy
C1:A-P0		B-P1
C2:B-P0		A-P1
C3:B-P2
Cooperative Sticky Strategy

对应的类叫做: org.apache.kafka.clients.consumer.ConsumerPartitionAssignor(最新的一种 2.4.1)

sticky 策略的特点:

  • 逻辑与 sticky 策略一致
  • 支持 cooperative 再均衡机制(再均衡的过程中,不会让所有消费者取消掉所有分区然后再进行重分配,影响到谁,就针对那个消费者进行即可)

消费者组再均衡流程

消费组在消费数据的时候,有两个角色进行组内的各事务的协调;

  • 角色 1: Group Coordinator (组协调器) 位于服务端(就是某个 broker)
  • 角色 2: Group Leader (组长) 位于消费端(就是消费组中的某个消费者)
GroupCoordinator 介绍

每个消费组在服务端对应一个 GroupCoordinator 其进行管理,GroupCoordinator 是 Kafka 服务端中用于管理消费组的组件。

消费者客户端中由 ConsumerCoordinator 组件负责与 GroupCoordinator 行交互;

ConsumerCoordinator 和 GroupCoordinator 最重要的职责就是负责执行消费者 rebalance 操作

eager 协议再均衡步骤细节

定位 Group Coordinator

coordinator 在我们组记偏移量的__consumer_offsets 分区的 leader 所在 broker 上

查找 Group Coordinator 的方式:

先根据消费组 groupid 的 hashcode 值计算它应该所在_consumer_offsets 中的分区编号:

在这里插入图片描述

Utils.abc(groupId.hashCode) % groupMetadataTopicPartitionCount
groupMetadataTopicPartitionCount 为 __consumer_offsets 的 分 区 总 数 , 这 个 可 以 通 过 broker 端 参 数
offset.topic.num.partitions 来配置,默认值是 50;

找到对应的分区号后,再寻找此分区 leader 副本所在 broker 节点,则此节点即为自己的 Grouping
Coordinator;

在这里插入图片描述

加入组 Join The Group

此阶段的重要操作之 1:选举消费组的 leader

private val members = new mutable.HashMap[String, MemberMetadata]

var leaderid = members.keys.head
set集合本身无序的,取头部的一个,自然也是无序的

消费组 leader 的选举,策略就是:随机!

此阶段的重要操作之 2:选择分区分配策略

最终选举的分配策略基本上可以看作被各个消费者支持的最多的策略,具体的选举过程如下:

(1)收集各个消费者支持的所有分配策略,组成候选集 candidates。

(2)每个消费者从候选集 candidates 找出第一个自身支持的策略,为这个策略投上一票。

(3)计算候选集中各个策略的选票数,选票数最多的策略即为当前消费组的分配策略(如果得票一样,那就以组长的为主)。

其实,此逻辑并不需要 consumer 来执行,而是由 Group Coordinator 来执行。

组信息同步 SYNC Group

此阶段,主要是由消费组 leader 将分区分配方案,通过 Group Coordinator 来转发给组中各消费者

在这里插入图片描述

心跳联系 HEART BEAT

进入这个阶段之后,消费组中的所有消费者就会处于正常工作状态。

各消费者在消费数据的同时,保持与 Group Coordinator的心跳通信。

消费者的心跳间隔时间由参数 heartbeat.interval.ms 指定,默认值为 3000 ,即这个参数必须比
session.timeout.ms 参 数 设 定 的 值 要 小 ; 一 般 情 况 下 heartbeat.interval.ms 的 配 置 值 不 能 超 过
session.timeout.ms 配置值的 1/3 。这个参数可以调整得更低,以控制正常重新平衡的预期时间;

如果一个消费者发生崩溃,并停止读取消息,那么 GroupCoordinator 会等待一小段时间确认这个消费者死亡之后才会触发再均衡。在这一小段时间内,死掉的消费者并不会读取分区里的消息。
这 个 一 小 段 时 间 由 session.timeout. ms 参 数 控 制 , 该 参 数 的 配 置 值 必 须 在 broker 端 参 数
group.min.session.timeout. ms (默认值为 6000 ,即 6 秒)和 group.max.session. timeout. ms (默认值为 300000 ,即 5 分钟)允许的范围内

再均衡流程

eager 协议的再均衡过程整体流程如下图:

在这里插入图片描述

特点:再均衡发生时,所有消费者都会停止工作,等待新方案的同步

Cooperative 协议的再均衡过程整体流程如下图:

在这里插入图片描述

特点:cooperative 把原来 eager 协议的一次性全局再均衡,化解成了多次的小均衡,并最终达到全局均衡的收敛状态

指定集合方式订阅主题

consumer.subscribe(Arrays.asList(topicl));

consumer.subscribe(Arrays.asList(topic2))

正则方式订阅主题

如果消费者采用的是正则表达式的方式(subscribe(Pattern))订阅, 在之后的过程中,如果有人又创建了新的主题,并且主题名字与正表达式相匹配,那么这个消费者就可以消费到新添加的主题中的消息。如果应用程序需要消费多个主题,并且可以处理不同的类型,那么这种订阅方式就很有效。

正则表达式的方式订阅的示例如下

consumer.subscribe(Pattern.compile ("topic.*" ));

利用正则表达式订阅主题,可实现动态订阅;

assign 订阅主题

消费者不仅可以通过 KafkaConsumer.subscribe() 方法订阅主题,还可直接订阅某些主题的指定分区;

在 KafkaConsumer 中提供了 assign() 方法来实现这些功能,此方法的具体定义如下:

public void assign(Collection<TopicPartition> partitions)

这个方法只接受参数 partitions,用来指定需要订阅的分区集合。示例如下:

consumer.assign(Arrays.asList(new TopicPartition ("tpc_1" , 0),new TopicPartition(“tpc_2”,1))) ;

subscribe 与 assign 的区别

通过 subscribe()方法订阅主题具有消费者自动再均衡功能 ;

在多个消费者的情况下可以根据分区分配策略来自动分配各个消费者与分区的关系。当消费组的消费者增加或减少时,分区分配关系会自动调整,以实现消费负载均衡及故障自动转移。

assign() 方法订阅分区时,是不具备消费者自动均衡的功能的;

其实这一点从 assign()方法参数可以看出端倪,两种类型 subscribe()都有 ConsumerRebalanceListener类型参数的方法,而 assign()方法却没有。

取消订阅

既然有订阅,那么就有取消订阅;
可以使用 KafkaConsumer 中的 unsubscribe()方法采取消主题的订阅,这个方法既可以取消通过subscribe( Collection)方式实现的订阅;也可以取消通过 subscribe(Pattem)方式实现的订阅,还可以取消通过 assign( Collection)方式实现的订阅。示例码如下

consumer.unsubscribe();

如果将 subscribe(Collection )或 assign(Collection)集合参数设置为空集合,作用与 unsubscribe()方法相同,如下示例中三行代码的效果相同:

consumer.unsubscribe();

consumer.subscribe(new ArrayList<String>()) ;

consumer.assign(new ArrayList<TopicPartition>());

消息的消费模式

Kafka 中的消费是基于拉取模式的。

消息的消费一般有两种模式:推送模式和拉取模式。推模式是服务端主动将消息推送给消费者,而拉模式是消费者主动向服务端发起请求来拉取消息。

Kafka 中的消息消费是一个不断轮询的过程,消费者所要做的就是重复地调用 poll( ) 方法, poll( )方法返回的是所订阅的主题(分区)上的一组消息。

对于 poll ( ) 方法而言,如果某些分区中没有可供消费的消息,那么此分区对应的消息拉取的结果就为空,如果订阅的所有分区中都没有可供消费的消息,那么 poll( )方法返回为空的消息集;

poll ( ) 方法具体定义如下:

public ConsumerRecords<K, V> poll(final Duration timeout)

超时时间参数 timeout ,用来控制 poll( ) 方法的阻塞时间,在消费者的缓冲区里没有可用数据时会发生阻塞。如果消费者程序只用来单纯拉取并消费数据,则为了提高吞吐率,可以把 timeout 设置为Long.MAX_VALUE;

消费者消费到的每条消息的类型为 ConsumerRecord

public class ConsumerRecord<K, V> {
    public static final long NO_TIMESTAMP = RecordBatch.NO_TIMESTAMP;
    public static final int NULL_SIZE = -1;
    public static final int NULL_CHECKSUM = -1;
    
    private final String topic;
    private final int partition;
    private final long offset;
    private final long timestamp;
    private final TimestampType timestampType;
    private final int serializedKeySize;
    private final int serializedValueSize;
    private final Headers headers;
    private final K key;
    private final V value;
    
    private volatile Long checksum;
    
    
topic partition 这两个字段分别代表消息所属主题的名称和所在分区的编号。
offset 表示消息在所属分区的偏移量。
timestamp 表示时间戳,与此对应的 timestampType 表示时间戳的类型。
timestampType 有两种类型 CreateTimeLogAppendTime ,分别代表消息创建的时间戳和消息追加
到日志的时间戳。
headers 表示消息的头部内容。
key value 分别表示消息的键和消息的值,一般业务应用要读取的就是 value ;
serializedKeySize、serializedValueSize 分别表示 key、value 经过序列化之后的大小,如果 key 为空,
则 serializedKeySize 值为 -1,同样,如果 value 为空,则 serializedValueSize 的值也会为 -1;
checksum 是 CRC32 的校验值。

示例代码片段

/**
* 订阅与消费方式 2
*/
TopicPartition tp1 = new TopicPartition("x", 0);
TopicPartition tp2 = new TopicPartition("y", 0);
TopicPartition tp3 = new TopicPartition("z", 0);
List<TopicPartition> tps = Arrays.asList(tp1, tp2, tp3);
consumer.assign(tps);
while (true) {
	ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
	for (TopicPartition tp : tps) {
		List<ConsumerRecord<String, String>> rList = records.records(tp);
		for (ConsumerRecord<String, String> r : rList) {
            r.topic();
            r.partition();
            r.offset();
            r.value();
            //do something to process record.
		}
	}
}

指定位移消费

有些时候,我们需要一种更细粒度的掌控,可以让我们从特定的位移处开始拉取消息,而KafkaConsumer 中的 seek() 方法正好提供了这个功能,让我们可以追前消费或回溯消费。
seek()方法的具体定义如下:

public void seek(TopicPartiton partition,long offset)

代码示例:

public class ConsumerDemo3 指定偏移量消费 {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.setProperty(ConsumerConfig.GROUP_ID_CONFIG,"g002");
        props.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"doit01:9092");
        props.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());
        props.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.clas
                s.getName());
        props.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest");
		// 是否自动提交消费位移
        props.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");
		// 限制一次 poll 拉取到的数据量的最大值
        props.setProperty(ConsumerConfig.FETCH_MAX_BYTES_CONFIG,"10240000");
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
		// assign 方式订阅 doit27-1 的两个分区
        TopicPartition tp0 = new TopicPartition("doit27-1", 0);
        TopicPartition tp1 = new TopicPartition("doit27-1", 1);
        consumer.assign(Arrays.asList(tp0,tp1));
		// 指定分区 0,从 offset:800 开始消费 ; 分区 1,从 offset:650 开始消费
        consumer.seek(tp0,200);
        consumer.seek(tp1,250);
		// 开始拉取消息
        while(true){
            ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(3000));
            for (ConsumerRecord<String, String> rec : poll) {
                System.out.println(rec.partition()+","+rec.key()+","+rec.value()+","+rec.offset()
                );
            }
        }
    }
}

自动提交消费者偏移量

Kafka 中默认的消费位移的提交方式是自动提交,这个由消费者客户端参数 enable.auto.commit 配置,默认值为 true 。当然这个默认的自动提交不是每消费一条消息就提交一次,而是定期提交,这个定期的周期时间由客户端参数 auto.commit.interval.ms 配置,默认值为 5 秒,此参数生效的前提是 enable. auto.commit 参数为 true。

在默认的方式下,消费者每隔 5 秒会将拉取到的每个分区中最大的消息位移进行提交。自动位移提交的动作是在 poll() 方法的逻辑里完成的,在每次真正向服务端发起拉取请求之前会检查是否可以进行位移提交,如果可以,那么就会提交上一次轮询的位移。

Kafka 消费的编程逻辑中位移提交是一大难点,自动提交消费位移的方式非常简便,它免去了复杂的位移提交逻辑,让编码更简洁。但随之而来的是重复消费消息丢失的问题。

  • 重复消费

假设刚刚提交完一次消费位移,然后拉取一批消息进行消费,在下一次自动提交消费位移之前,消费者崩溃了,那么又得从上一次位移提交的地方重新开始消费,这样便发生了重复消费的现象(对于再均衡的情况同样适用)。我们可以通过减小位移提交的时间间隔来减小重复消息的窗口大小,但这样并不能避免重复消费的发送,而且也会使位移提交更加频繁。

  • 丢失消息

按照一般思维逻辑而言,自动提交是延时提交,重复消费可以理解,那么消息丢失又是在什么情形下会发生的呢?我们来看下图中的情形:

拉取线程不断地拉取消息并存入本地缓存,比如在 BlockingQueue 中,另一个处理线程从缓存中读取消息并进行相应的逻辑处理。设目前进行到了第 y+l 次拉取,以及第 m 次位移提交的时候,也就是x+6 之前的位移己经确认提交了,处理线程却还正在处理 x+3 的消息;此时如果处理线程发生了异常,待其恢复之后会从第 m 次位移提交处,也就是 x+6 的位置开始拉取消息,那么 x+3 至 x+6 之间的消息就没有得到相应的处理,这样便发生消息丢失的现象。

在这里插入图片描述

手动提交消费者偏移量(调用 kafka api)

自动位移提交的方式在正常情况下不会发生消息丢失或重复消费的现象,但是在编程的世界里异常无可避免;同时,自动位移提交也无法做到精确的位移管理。在 Kafka 中还提供了手动位移提交的方式,这样可以使得开发人员对消费位移的管理控制更加灵活。

很多时候并不是说拉取到消息就算消费完成,而是需要将消息写入数据库、写入本地缓存,或者是更加复杂的业务处理。在这些场景下,所有的业务处理完成才能认为消息被成功消费;

手动的提交方式可以让开发人员根据程序的逻辑在合适的时机进行位移提交。开启手动提交功能的前提是消费者客户端参数 enable.auto.commit 配置为 false ,示例如下:

props.put(ConsumerConf.ENABLE_AUTO_COMMIT_CONFIG, false);

手动提交可以细分为同步提交和异步提交,对应于 KafkaConsumer 中的 commitSync()和commitAsync()两种类型的方法。

  • 同步提交的方式

commitSync()方法的定义如下:

/**
* 手动提交 offset
*/
while (true) {
	ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
	for (ConsumerRecord<String, String> r : records) {
		//do something to process record.
	}
	consumer.commitSync();
}

对于采用 commitSync()的无参方法,它提交消费位移的频率和拉取批次消息、处理批次消息的频率是一样的,如果想寻求更细粒度的、更精准的提交,那么就需要使用 commitSync()的另一个有参方法,具体定义如下:

public void commitSync(final Map<TopicPartitionOffsetAndMetadata> offsets

示例代码如下:

while (true) {
	ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
	for (ConsumerRecord<String, String> r : records) {
		long offset = r.offset();
		//do something to process record.
		
		TopicPartition topicPartition = new TopicPartition(r.topic(), r.partition());
		consumer.commitSync(Collections.singletonMap(topicPartition,new
OffsetAndMetadata(offset+1)));
	}
}

提交的偏移量 = 消费完的 record 的偏移量 + 1

因为,__consumer_offsets 中记录的消费偏移量,代表的是,消费者下一次要读取的位置!!!

  • 异步提交方式

异步提交的方式( commitAsync())在执行的时候消费者线程不会被阻塞;可能在提交消费位移的结果还未返回之前就开始了新一次的拉取。异步提交可以让消费者的性能得到一定的增强。commitAsync 方法有一个不同的重载方法,具体定义如下:

在这里插入图片描述

示例代码

/**
* 异步提交 offset
*/
while (true) {
	ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
	for (ConsumerRecord<String, String> r : records) {
		long offset = r.offset();
		
		//do something to process record.
		TopicPartition topicPartition = new TopicPartition(r.topic(), r.partition());
		consumer.commitSync(Collections.singletonMap(topicPartition,new
OffsetAndMetadata(offset+1)));
		consumer.commitAsync(Collections.singletonMap(topicPartition, new
OffsetAndMetadata(offset + 1)), new OffsetCommitCallback() {
	@Override
	public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
				if(e == null ){
					System.out.println(map);
				}else{
					System.out.println("error commit offset");
				}
			}
		});
	}
}

手动提交位移(时机的选择)

  • 数据处理完成之前先提交偏移量

可能会发生漏处理的现象(数据丢失)

反过来说,这种方式实现了: at most once 的数据处理(传递)语义

  • 数据处理完成之后再提交偏移量

可能会发生重复处理的现象(数据重复)

反过来说,这种方式实现了: at least once 的数据处理(传递)语义

当然,数据处理(传递)的理想语义是: exactly once(精确一次)

Kafka 也能做到 exactly once(基于 kafka 的事务机制)

消费者提交偏移量方式的总结

consumer 的消费位移提交方式:

全自动

  • auto.offset.commit = true
  • 定时提交到 consumer_offsets

半自动

  • auto.offset.commit = false;
  • 然后手动触发提交 consumer.commitSync()
  • 提交到 consumer_offsets

全手动

  • auto.offset.commit = false;
  • 写自己的代码去把消费位移保存到你自己的地方 mysql/zk/redis
  • 提交到自己所涉及的存储;初始化时也需要自己去从自定义存储中查询到消费位移

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1248195.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

github国内访问小解(windows)

git 下载安装 使用 github 前必须确保电脑上已经安装了 Git&#xff0c;可以从 Git 官方网站去下载。 官方的网站在国内访问会比较慢&#xff0c;这里可以选择国内镜像&#xff1a;https://registry.npmmirror.com/binary.html?pathgit-for-windows/ github 之旅 确认电脑已…

使用向日葵开机棒进行远程开机

文章目录 1\. 前言2\. 说明3\. 开机棒设置4\. 电脑端设置4.1. 电脑端允许网卡唤醒4.1.1. 关闭设备节能4.2. 将电脑端设备加入设备列表 5\. 手机端5.1. 添加开机棒5.2. 绑定主机5.2.1. 添加成功的主机 6\. 唤醒 1. 前言 如果我们出差在外或者人不在实验室&#xff0c;如果可以使…

Spring 七大组件

文章目录 Spring 七大组件 Spring 七大组件 核心容器(Spring core) 核心容器提供Spring框架的基本功能。Spring以bean的方式组织和管理Java应用中的各个组件及其关系。Spring使用BeanFactory来产生和管理Bean&#xff0c;它是工厂模式的实现。BeanFactory使用控制反转(IOC)模式…

Linux python安装 虚拟环境 virtualenv,以及 git clone的 文件数据, 以及 下资源配置

根目录创建 venvs 文件夹 sudo mkdir /venvs 进入 /venvs 目录 cd /venvsp 创建虚拟环境&#xff0c;前提要按照 python3 安装 的 命令 sudo apt install python3 sudo python3 -m venv 虚拟环境名 激活虚拟环境 source /venvs/zen-venv/bin/activate 安装flask pip install fl…

测试工具JMeter的使用

目录 JMeter的安装配置 测试的性能指标 TPS 响应时长 并发连接 和 并发用户 CPU/内存/磁盘/网络 负载 性能测试实战流程 JMeter JMeter快速上手 GUI模式 运行 HTTP请求默认值 录制网站流量 模拟间隔时间 Cookie管理器 消息数据关联 变量 后置处理器 CSV 数据文…

Spring AOP:什么是AOP? 为什么要用AOP?如何学习AOP?

文章目录 &#x1f386;前言1.为什么要用 AOP3.如何学习去 AOP?3.1 AOP 的组成切面&#xff08;Aspect&#xff09;连接点&#xff08;Join Point&#xff09;切点&#xff08;Pointcut&#xff09;通知&#xff08;Advice&#xff09; 3. Spring AOP 实现3.1 普通的方式实现 …

切换服务器上自己用户目录下的 conda 环境和一个外部的 Conda 环境

如果我们有自己的 Miniconda 安装和一个外部的 Conda 环境&#xff08;比如一个全局安装的 Anaconda&#xff09;&#xff0c;我们可以通过修改 shell 环境来切换使用它们。这通常涉及到更改 PATH 环境变量&#xff0c;以便指向你想要使用的 Conda 安装的可执行文件&#xff1a…

二维数值型数组例题2

1、内部和 题目描述 给定一个m行n列的二维矩阵&#xff0c;求其内部元素和 输入要求 第一行为两个整数&#xff1a;m和n&#xff08;0<m,n<10&#xff09;&#xff0c;接下来输入m*n的二维矩阵 输出要求 二维矩阵内部元素和 输入样例 3 3 1 2 3 4 5 6 7 8 9 …

2023年11月18日~11月24日周报(调试OpenFWI代码)

目录 一、前言 二、完成情况 2.1 OpenFWI工程文件框架的理解 2.2 InversionNet网络的理解 2.3 dataset.py的理解 三、遇到的部分问题及解决 3.1 数据读取 3.2 Dataloader中设置num_workers-MemoryError 3.3 RuntimeError: CUDA out of memory 3.4 loss值的变化 四、相…

MySQL介绍及安装

MySQL介绍及安装 一、MySQL概述 1、关系型数据库与非关系型数据库 RDBMS&#xff08;relational database management system&#xff09;&#xff0c;既关系型数据库管理系统。 简单来说&#xff0c;关系型数据库&#xff0c;是指采用了二维表格来组织数据的数据库。 扩展…

Codeforces Round 786 (Div. 3) D. A-B-C Sort

D. A-B-C Sort 步骤 1 &#xff1a;当 a不为空时&#xff0c;从 a中取出最后一个元素&#xff0c;并将其移动到数组 b的中间。如果 b 当前长度为奇数&#xff0c;则可以选择&#xff1a;将 a 中的元素放到 b 中间元素的左边或右边。结果&#xff0c; a 变空&#xff0c; b 由 n…

SAS9.2软件“OLE:对象的类没有在注册数据库中注册“问题的解决. 2023-11-24

操作系统平台: win7 sp1 32bit (6.1.7601.26321 (Win7 RTM)) 1.安装依赖库(必须) Microsoft Visual C 2005 Redistributable Microsoft Visual C 2008 Redistributable 官方下载地址: 最新受支持的 Visual C 可再发行程序包下载 | Microsoft Learn 2.以管理员权限启动cmd.e…

Python常见报错以及解决方案梳理,快滚进收藏夹里吃灰吧!

前言 使用python难免会出现各种各样的报错&#xff0c;以下是Python常见的报错以及解决方法&#xff08;持续更新&#xff09;&#xff0c;快进入收藏吃灰吧&#xff01; AttribteError: ‘module object has no attribute xxx 描述:模块没有相关属性。可能出现的原因: 1.命…

[黑马程序员SpringBoot2]——原理篇1

目录&#xff1a; bean的加载方式(—)bean的加载方式(二)bean的加载方式(三)FactoryBeanproxyBeanMethod属性bean的加载方式(四)bean的加载方式(五)bean的加载方式(六)bean的加载方式(七)bean的加载方式(八)bean加载控制&#xff08;编程式)bean加载控制&#xff08;注解式)be…

加速 Selenium 测试执行最佳实践

Selenium测试自动化的主要目的是加快测试过程。在大多数情况下&#xff0c;使用 Selenium 的自动化测试比手动测试执行得特别好。在实际自动化测试实践中&#xff0c;我们有很多方式可以加速Selenium用例的执行。 我们可以选择使用不同类型的等待、不同类型的 Web 定位器、不同…

libmosquitto库的一个bug,任务消息id(mid)分配后不起作用

代码如图所示: 当订阅了所有主题后,每个主题的mid是他们的下标索引加100的数字,可是实际打印出来的值是: mid依然是1,2,这个参数在这里失效了,不知道是bug还是mqtt的什么机制?

2023 最新 PDF.js 在 Vue3 中的使用

因为自己写业务要定制各种 pdf 预览情况&#xff08;可能&#xff09;&#xff0c;所以采用了 pdf.js 而不是各种第三方封装库&#xff0c;主要还是为了更好的自由度。 一、PDF.js 介绍 官方地址 中文文档 PDF.js 是一个使用 HTML5 构建的便携式文档格式查看器。 pdf.js 是社区…

【Python进阶笔记】md文档笔记第6篇:Python进程和多线程使用(图文和代码)

本文从14大模块展示了python高级用的应用。分别有Linux命令&#xff0c;多任务编程、网络编程、Http协议和静态Web编程、htmlcss、JavaScript、jQuery、MySql数据库的各种用法、python的闭包和装饰器、mini-web框架、正则表达式等相关文章的详细讲述。 全套md格式笔记和代码自…

2015年全国硕士研究生入学统一考试管理类专业学位联考数学试题——解析版

文章目录 2015 级考研管理类联考数学真题一、问题求解&#xff08;本大题共 15 小题&#xff0c;每小题 3 分&#xff0c;共 45 分&#xff09;下列每题给出 5 个选项中&#xff0c;只有一个是符合要求的&#xff0c;请在答题卡上将所选择的字母涂黑。真题&#xff08;2015-01&…

白嫖CTG4.0

大家好&#xff0c;到点了我来给各位大佬献策CTG&#xff0c;不是花钱买不起&#xff0c;而是免费更有性价比&#xff0c;哈哈哈不调侃了我们自此开始正文&#xff0c;咱们主打的就是一个分享是一种态度 当然我更希望大家支持国产对国产有自己的信心&#xff08;文心一言&…