Kafka消费者客户端详解

文章目录

    • 引言
    • 1. 消费组
        • 消费组(Consumer Group)与分区(Partition)
        • 消费组中只有一个消费者
        • 消费组中消费者数量与分区数量相同
        • 消费组中消费者数量少于分区数
        • 消费组中消费者数量大于分区数
        • 多个消费组订阅相同的主题
    • 2. 分区分配策略
      • 2.1 RangeAssignor分区分配策略
        • 4个分区两个消费者
      • 2.2 RoundRobinAssignor分区分配策略
        • 消费组内每个消费者都订阅了两个主题
        • 消费组内消费者订阅的主题不同
      • 2.3 StickAssignor分区分配策略
    • 3. 两种消息投递模式
      • 3.1 点对点模式
      • 3.2 发布/订阅模式
    • 4. 消费者Java客户端
        • 必备参数配置
      • 4.1 订阅主题
      • 4.2 订阅分区
      • 4.3 取消订阅
    • 5. 再均衡
      • 5.1 再均衡监听器
    • 6. 消息消费
      • 6.1 poll消费
      • 6.2 指定位移消费
      • 6.3 控制或关闭消费
    • 7. 位移提交
      • 7.1 位移提交时机
        • 拉取完消息立即提交
        • 消费完所有消息后才提交
        • 自动提交-定期提交
        • 手动提交
    • 8. 反序列化器
      • 8.1 自定义反序列化器
    • 9. 拦截器
    • 10. 多线程
    • 参考文献

引言

  • 消费者(Consumer)负责订阅Kafka中的主题,并且从订阅的主题上拉取消息。
  • 与其他一些消息中间件不同,在Kafka的消费理念中还有一层消费组(Consumer Group)的概念;

1. 消费组

  • 一个消费组(Group)内有一个到多个消费者(Consumer);
  • 一个消费者(Consumer)只属于一个消费组;
  • 一个消费组可以同时订阅多个主题;
  • 不同的消费组可以订阅相同的主题;
  • 一个消费组内的不同消费者可以订阅不同的主题;当然也可以订阅相同的主题;
  • 当消息发布到主题后,只会被投递到订阅它的每个消费组中的一个消费者;

消费组(Consumer Group)与分区(Partition)

  • 消费组实际上是和主题相对应的,消费组里的消费者根据分区分配策略会把主题下的分区给分配了,每个消费者可以消费分配的分区里的消息;
  • 主题下包含多个分区,向主题发送消息时,消息会发往其中一个分区;
    • 具体是将消息发送到哪一个分区,与消息的keypartition属性有关;
  • 消费组订阅主题时,消费组中的一个消费者会接收到发往该主题的消息;
    • 具体是哪一个消费者接收到消息,与分区分配策略有关;
    • 当消费组中只有一个消费者时,它接收所有分区的消息,当消费者个数与分区个数相同时,一个消费者接收一个分区的消息;更普通的情况是一个消费者接收多个分区的消息,并且尽量使分配均匀

消费组中只有一个消费者

  • 如果一个消费组(Group)里只有一个消费者(Consumer),那么这个消费者(Consumer)可以消费发送到所有分区里的消息;
    Kafka消费者客户端详解_第1张图片

消费组中消费者数量与分区数量相同

  • 如果Group中的Consumer数量与分区数相同,则每个Consumer分配一个分区,当消息发送到一个分区时,分配对应分区的Consumer可以消费消息;
    Kafka消费者客户端详解_第2张图片

消费组中消费者数量少于分区数

  • 如果Group中Consumer数量少于分区数,则按照分区分配策略将分区尽可能均匀的分配给各个Consumer,每个Consumer可以消费发送给对应分区的消息

Kafka消费者客户端详解_第3张图片

消费组中消费者数量大于分区数

  • 如果Group中Consumer数量大于分区数,那么会有一部分Consumer分配不到分区,其他Consumer一对一分配分区;

多个消费组订阅相同的主题

  • 多个消费组订阅相同的主题时,消费组之间互不影响,发往主题的消息会同时被两个消费组接收到,具体是消费组内哪个Consumer接收到消息由分区分配策略决定;

Kafka消费者客户端详解_第4张图片

2. 分区分配策略

  • Kafka提供了消费者客户端参数partition.assignment.strategy来设置消费者与订阅主题之间的分区分配策略。
  • Kafka提供了三种分区分配策略:RangeAssignorRoundRobinAssignorStickyAssignor
  • partition.assignment.strategy的默认参数是org.apache.kafka.clients.consumer.RangeAssignor,即默认使用RangeAssignor分区分配策略;

2.1 RangeAssignor分区分配策略

  • RangeAssignor分区分配策略的原理是:按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费组;
  • RangeAssignor会将消费组内所有订阅这个主题的消费者按名称的字典顺序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典靠前的消费者会被多分配一个分区;

4个分区两个消费者

假设主题T0有4个分区:P0、P1、P2、P3,消费组内有两个消费者:C0、C1,则RangeAssinger策略分区分配方案是:

T0: P0   P1   P2  P3
    |    |    |   |
    C0   C0   C1  C1

假设主题T1有3个分区:P0、P1、P2 ,消费者内有两个消费者:C0、C1,则RangeAssigner策略的分区分配方案是:

T1: P0  P1  P2
    |   |   |
    C0  C0  C1
  • 可见,分区是按范围分配给每个消费者的;

2.2 RoundRobinAssignor分区分配策略

  • RoundRobinAssignor分区分配策略的原理是:将消费组内所有消费者及消费组订阅的所有主题的分区按照字典序排序,然后通过轮询方式逐个将分区依次分配给每个消费者;
  • 注意,RoundRobinAssignor会把消费组订阅的所有主题的所有分区排序;

消费组内每个消费者都订阅了两个主题

  • 假设消费组中有两个消费者:C0、C1;
  • 每个消费者都订阅了主题T0和T1;
  • 每个主题中都有3个分区;
  • 则最终的分配结果是:
TO: P0  P1  P2
T1: P1  P1  P2

消费者排序: C0  C1
所有分区排序: T0P0  T0P1  T0P2  T1P0  T1P1  T1P2

轮询分配:T0P0  T0P1  T0P2  T1P0  T1P1  T1P2
            |     |     |     |     |     |
           C0    C1    C0    C1    C0    C1

最终分配结果:
   消费者C0: T0P0、T0P2、T1P1
   消费者C1: T0P1、T1P0、T1P2
  • 可见,分区是轮询着分配给各个消费者的;

消费组内消费者订阅的主题不同

  • 假设消费组中有3个消费者:C0、C1、C2;
  • 共订阅了三个主题T0、T1、T2,这三个主题分别有1、2、3个分区;
  • 并不是三个消费者都订阅了这三个主题;
    • C0只订阅了主题T0;
    • C1同时订阅了主题T0和T1;
    • C2同时订阅了主题T0、T1和T2;
消费组: C0、C1、C2

主题T0:  P0           <-订阅--C0  <-订阅--C1    <-订阅--C2
主题T1:  P0  P1                   <-订阅--C1    <-订阅--C2
主题T2:  P0  P1  P2                             <-订阅--C2

轮询分配: T0P0  T1P0  T1P1  T2P0  T2P1  T2P2
             |     |     |     |     |     |
            C0    C1    C2    C2    C2    C2

最终分配结果:
   消费者C0: T0P0
   消费者C1: T1P0
   消费者C2: T1P1、T2P0、T2P1、T2P2
  • 可见,在这种情况下RoundRobinAssignor策略分配的并不均匀,这样分配其实并不是最优解;

2.3 StickAssignor分区分配策略

StickAssignor分配策略又叫粘性分配策略,它有两个目标:

  • (1) 分区的分配要尽可能均匀;
  • (2) 分区的分配尽可能与上次分配的保持相同;

当两者发生冲突时,第一个目标优先于第二个目标;

StickAssignor分配策略的实现比较复杂(书上并没有讲解,只给出了分配结果,网络上也没有找到好的讲解),根据分配结果可以有以下推测:

  1. 当消费组中的所有消费者订阅的主题相同时,初始分配结果与RoundRobinAssignor分配策略的分配结果相同;
  2. 当消费组中的消费者订阅的主题不相同时,初始分配结果与RoundRobinAssignor不同,分配结果更加均匀;
  3. 当有一个消费者退出消费组时,RoundRobinAssignor会重新分配所有的分区,而StickAssignor会将退出的消费者分配的分区分配给其他消费者,未退出的消费者之前分配的分区尽量不会变化(粘性);

3. 两种消息投递模式

对于消息中间件而言,一般有两种消息投递模式:点对点模式(P2P)发布/订阅模式(Pub/Sub)

3.1 点对点模式

点对点模式中,生产者将消息发送到队列中,消费者从队列中取出并消费消息,消息被消费后队列不再存储,队列支持多个消费者。该模式有以下特点:

  • 消息不可重复消费;
  • 一个消息只会有一个消费者可以消费;

Kafak不是一种典型的点对点模式,但是通过合理的使用消费组(Consumer Group),可以实现点对点模式:

  • 如果订阅主题的所有的消费者(Consumer)都隶属于同一个消费组(Group),那么发送给主题的所有消息都只会投递给其中一个消费者(Consumer),即每条消息只会被一个消费者处理,这就相当于点对点模式的应用;

3.2 发布/订阅模式

发布订阅模式中,生产者将消息发布到topic中,同时有多个消费者可以消费到该消息。该模式的特点是:

  • 消息可以重复消费;
  • 一个消息会被所有订阅者消费;

Kafka是典型的发布/订阅模式,但是要实现发布/订阅模式还需要正确使用消费组的概念:

  • 如果订阅主题的所有消费者(Consumer)都隶属于不同的消费组(Consumer Group),那么发送给主题的所有消息都会被广播给所有的消费者(Consumer),即每条消息会被所有的消费者处理,这就相当于发布/订阅模式的应用;

4. 消费者Java客户端



    org.apache.kafka
    kafka-clients
    2.0.0

public class ConsumerFastStart {
    public static final String brokerList = "localhost:9092";
    public static final String topic = "topic-learn";
    public static final String groupId = "group.demo";

    public static final AtomicBoolean isRunning = new AtomicBoolean(true);

    public static Properties initProperties() {
        Properties proper = new Properties();
        proper.put("bootstrap.servers", brokerList);
        proper.put("key.deserializer", StringDeserializer.class.getName());
        proper.put("value.deserializer",StringDeserializer.class.getName());
        proper.put("client.id", "consumer.client.id.demo");

        // 设置消费者所属的消费组的名称
        proper.put("group.id", groupId);

        return proper;
    }

    public static void main(String[] args) {
        Properties proper = initProperties();
        // 创建一个消费者客户端实例
        KafkaConsumer consumer = new KafkaConsumer<>(proper);
        // 订阅主题
        consumer.subscribe(Collections.singletonList(topic));

        // 循环消费消息
        System.out.println("====== 接收消息 ======");
        // 使用 AtomicBoolean来作为while循环,可以通过 isRunning.set(false)来结束下一轮循环
        while (isRunning.get()) {
            ConsumerRecords records = consumer.poll(Duration.ofMillis(1000));
            for (ConsumerRecord record : records) {
                System.out.println(record.value());
            }
        }
    }
}
  • bootstrap.serversgroup.idkey.deserializervalue.deserializer是消费者客户端必填的参数;
  • 为了防止参数名记错,可以使用ConsumerConfig里的常量;
  • 消费者和生产者使用的常量不同,生产者使用的常量是ProducerConfig

必备参数配置

  • bootstrap.servers:释义和生产者客户端KafkaProducer中的相同,指定连接kafka集群所需的broker地址清单;
  • group.id:消费者隶属的消费组,默认值为’’,这个是必填值,如果设置为空,会报异常;
  • key.deserializervalue.deserializer:用来指定消息中keyvalue所需的反序列化器,参数无默认值;需要和生产者客户端KafkaProducer中配置的key.serializervalue.serializer相对应;
  • client.id:用来设定KafkaConsumer对应的客户端id,默认值为’’,如果客户端不设置,则KafkaConsumer会自动生成一个非空字符串;
  • 注意:这个参数不是必备参数;

4.1 订阅主题

KafkaConsumer订阅主题的方法有以下几个:

/**
 * KafkaConsumer订阅主题的方法
 */
public void subscribe(Collection topics);
public void subscribe(Collection topics, ConsumerRebalanceListener listener)

public void subscribe(Pattern pattern);
public void subscribe(Pattern pattern, ConsumerRebalanceListener listener);
  • 消费者可以使用集合的方式同时订阅多个主题;但是如果是多次调用订阅方法,则会以最后一次订阅的为准;
  • 消费者还可以使用正则表达式的方式订阅主题;
  • 在订阅方法中可以传递一个再均衡监听器;再均衡监听器用来设定发生再均衡动作前后的一些准备或收尾动作;

4.2 订阅分区

KafkaConsumer还可以通过assign方法直接订阅分区

/**
 * KafkaConsumer的 assign方法
 */
public void assign(Collection partitions);
  • 可以同时订阅多个分区;
  • 订阅分区时,没有传递再均衡监听器(订阅主题时才会有再均衡);

其中,TopicPartition对象代表主题的分区

/**
 * 主题分区对象
 */
 public final class TopicPartition implements Serializable {

    private int hash = 0;
    private final int partition;
    private final String topic;
    
    // ... 省略内部方法
}

4.3 取消订阅

KafkaConsumer中的unsubscribe方法可以用来取消订阅,既可以取消通过subscribe订阅的主题,也可以取消通过assign方法直接订阅的分区;

/**
 * KafkaConsumer的unsubscribe方法
 */
 public void unsubscribe();

5. 再均衡

  • 再均衡是指分区的所属权从一个消费者移动到另一个消费者的行为;
  • 在多个消费者的情况下,根据分区分配策略来自动分配消费者与分区的关系;
  • 当消费组内的消费者增加或减少时,分区分配关系会自动调整,以实现消费负载均衡及故障自动转移;
  • 通过subscribe方法订阅主题具有消费者自动再均衡的功能;而通过assign方法订阅分区时,是不具备消费者自动均衡功能的;
  • 再均衡为消费组具备高可用性和伸缩性提供保障,使我们可以既方便又安全地删除消费组内的消费者或往消费组内添加消费者;
  • 不过,在发生再均衡期间,消费组内的消费者是无法读取消息的,即在发生再均衡期间,消费组会变得不可用;
  • 另外,当一个分区被重新分配给另一个消费者时,消费者当时的状态也会丢失,即还没有来得及提交的消费位移会丢失,新的消费者会重新消费还未提交消费位移的消息;

5.1 再均衡监听器

再均衡监听器接口

/**
 * 再均衡监听器接口
 */
public interface ConsumerRebalanceListener {
     
    void onPartitionsRevoked(Collection<TopicPartition> partitions);
    
void onPartitionsAssigned(Collection<TopicPartition> partitions);
}
  • 再均衡监听器用来设定发生再均衡动作前后的一些准备或收尾动作;
  • onPartitionsRevoked方法:在再均衡开始之前和消费者停止读取消息之后被调用,参数partitions表示再均衡所分配的分区;
  • onPartitionsAssigned方法:在重新分配分区之后和消费者开始读取消费之前被调用。参数partitions表示再均衡所分配的分区;

6. 消息消费

消息的消费一般有两种模式:推模式和拉模式:

  • 推模式:服务端主动将消息推送给消费者;
  • 拉模式:消费者主动向服务器发起请求来拉取消息;

6.1 poll消费

Kafka中的消费是基于拉模式的;

  • Kafka中的消息消费是一个不断轮询的过程,消费者所要做的就是重复地调用poll()方法,而poll方法返回的是所订阅的主题(分区)上的一组消息;
/**
 * KafkaConsumer的poll方法
 */
@Deprecated
public ConsumerRecords poll(final long timeout);

public ConsumerRecords poll(final Duration timeout);

// 内部调用的方法
private ConsumerRecords poll(final long timeoutMs, final boolean includeMetadataInTimeout);
  • KafkaConsumer对外提供的poll方法有两个:poll(final long timeout)poll(final Duration timeout),参数传递一个超时时间,用来控制poll方法阻塞的时间,在消费者的缓冲区里没有可用数据时,会发生阻塞;
  • 其中第一个已经标记为Deprecated,被第二个替代;因为第一个方法的时间单位固定为毫秒,第二个方法可以根据Duration中的ofMillis()ofSecondsofHours等多种不同的方法指定不同的时间单位;
  • Duration是从jdk1.8开始添加的时间内,在包java.time包下;
  • poll方法内部会进行分区分配的逻辑,如果将参数设置为0,则该方法会立刻返回,内部的分区分配逻辑会来不及实施;
  • poll方法返回的是ConsumerRecords,它用来表示一次拉取操作所获得的消息集,内部包含了若干ConsumerRecord(不带s);
/**
 * 消息集 ConsumerRecords 内部的方法
 */
 public class ConsumerRecords implements Iterable> {
    
    // 提取消息集中指定分区的消息
    public List> records(TopicPartition partition);

    // 提取消息集中指定主题的消息
    public Iterable> records(String topic);

    // 查看拉取的消息集中的分区列表
    public Set partitions();

    // 循环遍历消息集中的消息
    public Iterator> iterator();

    // 返回消息集中消息的个数
    public int count();
    
    // 判断消息集是否为空
    public boolean isEmpty();

    // ...
}

消费者拉取的消息ConsumerRecord:

/**
 * 消费者客户端拉取的消息 ConsumerRecord
 */
 public class ConsumerRecord {
    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;
    
    // ... 省略内部方法
}
  • 泛型K代表key的类型;
  • 泛型V代表value的类型;

6.2 指定位移消费

  • 消费者消费完消息后会进行消费位移提交,Kafka将消费位移持久化,有了消费位移的持久化,才能使消费者在关闭崩溃再均衡时,能够让接替的消费者根据存储的消费位移继续进行消费;
  • 但是当消费者找不到所记录的消费位移时(比如,新的消费组建立,或者一个新的消费者订阅了新的主题后),就会根据消费者客户端参数auto.offset.reset的配置来决定从何处开始进行消费;

auto.offset.reset参数取值:

  • latest,默认参数,会从分区末尾开始消费消息;
  • earlist,会从起始处开始消费;
  • none,表示出现查不到消费位移的时候,既不从最新的消息位置处开始消费,也不从最早的消息位置处开始消费,而是会报出NoOffsetForPartitionException异常;
  • 说明:除了查找不到消费位移,位移越界也会触发auto.offset.reset参数的执行;

KafkaConsumer的seek方法提供可以从特定的位移处开始拉取消息:

/**
 * KafkaConsumer的seek方法
 */
public void seek(TopicPartition partition, long offset);

public void seekToBeginning(Collection partitions);

public void seekToEnd(Collection partitions);
  • seek方法的参数partitions表示分区,offset参数用来指定从分区的哪个位置开始消费;
  • seek方法只能重置消费者分配到的分区的消费位置,而消费者的分区分配是在poll方法调用过程中实现的,所以在执行seek方法之前需要先执行一次poll方法,等到分配到分区之后才可以重置消费位移;
  • 注意:如果poll方法的时间参数设置为0,则会立刻返回,那么方法内部的分区分配逻辑会来不及实施;

代码:使用seek()方法从分区末尾消费

/**
 * 使用seek()方法从分区末尾消费
 */

public static void seekTest() {
    Properties proper = initProperties();
    KafkaConsumer consumer = new KafkaConsumer<>(proper);
    consumer.subscribe(Arrays.asList(topic));
    Set assignment = new HashSet<>();
    while (assignment.size() == 0) {
        consumer.poll(Duration.ofMillis(100));
        
        // KafkaConsumer的 assignment 方法获取消费者所分配到的分区信息
        assignment = consumer.assignment();
    }
    
    // KafkaConsumer的 endOffsets 方法用来获取指定分区的末尾的消息位置,返回一个 Map
    Map offsets = consumer.endOffsets(assignment);
    for (TopicPartition tp : assignment) {
        consumer.seek(tp, offsets.get(tp));
    }
}
  • KafkaConsumer的assignment方法获取消费者所分配到的分区信息;
  • KafkaConsumer的endOffsets方法用来获取指定分区的末尾的消息位置,返回一个 Map;
  • endOffsets方法还可以接收一个时间参数Duration timeout,指定等待获取的超时时间,如果没有指定timeout,那么等待时间由客户端参数request.timout.ms来设置,默认是30000
  • KafkaConsumer里与endOffsets方法对应的还有beginningOffsets方法;
  • KafkaConsumer还直接提供了seekToBeginningseekToEnd方法来实现直接从分区的开头或末尾开始消费;

6.3 控制或关闭消费

  • KafkaConsumer提供了对消费速度进行控制的方法,可以暂停某些分区的消费而先消费其他分区,当达到一定条件时再恢复这些分区的消费;
  • KafkaConsumer中使用pause()resume()方法来分别实现:暂停某些分区在拉取操作时返回数据给客户端;恢复某些分区在向客户端返回数据;
/**
 * KafkaConsumer的pause()和resume()方法
 */
public void pause(Collection partitions);

public void resume(Collection partitions);

KafkaConsumer是线程不安全的,但是有一个wakeup()方法可以从其他线程里安全调用,调用wakeup()方法可以退出poll()方法的逻辑,并抛出WakeupException异常,我们不需要处理该异常,它只是一种跳出循环的方式;

  • 跳出循环后一定要显式的执行关闭动作以释放运行过程中占用的各种系统资源,包括内存资源、Socket连接等,KafkaConsumer提供了close()方法来实现关闭;
/**
 * KafkaConsumer的wakeup()方法和close()方法
 */
public void wakeup();
 
public void close();
public void close(Duration timeout);
@Deprecated
public void close(long timeout, TimeUnit timeUnit)

7. 位移提交

  • 对于Kafka的分区而言,分区中的每条消息都有唯一的offset,用来标识消息在分区中对应的位置;
  • 对于消费者而言,也有一个offset,表示当前消费到分区中的某个消息所在的位置;
  • KafkaConsumer每次调用poll()方法时,返回的是还没有消费过的消息集,要做到这一点就要记录上一次消费时的消费位移;并且这个消费位移必须做持久化保存,而不是单单保存在内存中;这样在消费者重启、新的消费者加入、再均衡发生时,都能够知晓之前的消费位移,然后继续消费后续的消息;
  • 在旧版消费者客户端中,消费位移是存储在Zookeeper中的,而在新消费者客户端中,消费位移存储在Kafka内部的主题__consumer_offsets中;
  • 消费者在消费完消息之后,需要执行消费位移的提交,提交的是下一条需要拉取的消息的位置
  • ConsumerRecord消息中有属性offset记录本次消息在分区的偏移量,KafkaConsumercommitted()方法获取提交的消费位移,比获取的消息的最大偏移量大1;

7.1 位移提交时机

位移提交时机的把握也很讲究,不同的提交时机可能造成重复消费消息丢失的现象;

拉取完消息立即提交

如果拉取完消息还未做消息处理前,就立即提交消费位移,有可能造成消息丢失现象:

  • 例如:当前poll()操作拉取的消息集为[x+2,x+7]其中x+2代表上一次提交的消费位移,如果拉取到消息之后就进行了位移提交,即提交了x+8(下一次需要消费的消息位移),那么假如当前消费到了x+5时,消费者遇到了异常,在故障恢复后,消费者重新拉取消息,因为已经提交了消费位移x+8,所以重新拉取的消息是从x+8开始的,这样会导致x+5x+7之间的消息未被处理,如此便发生了消息丢失现象;
  • 即,消费者拉取了消息,然后提交消费位移,但是在处理消息过程中遇到了异常,在故障恢复后再拉取消息就会拉取提交位移后的消息,上次拉取的消息存在部分未处理的情况;

消费完所有消息后才提交

如果位移提交动作是在消费完所有拉取的消息后才执行,有可能造成重复消费现象

  • 例如:当前poll()操作拉取的消息集为[x+2,x+7]其中x+2代表上一次提交的消费位移,当消费到x+5时遇到了异常,在故障恢复后,重新拉取消息,因为本次消费位移还未提交,则重新拉取的消息是从x+2开始的,也就是说x+2x+4的消息又重新消费了一遍,故而发生了重新消费的现象;

自动提交-定期提交

  • Kafka中默认的消费位移的提交方式是自动提交,这个由消费者客户端参数enable.auto.commit配置,默认值为true;
  • 默认的自动提交是定期提交:消费者每隔5秒(由参数auto.commit.interval.ms控制)会将拉取的每个分区中最大的消息位移进行提交。自动位移提交的动作是在poll()方法的逻辑里完成的,在每次真正向服务器发起拉取请求之前会检查是否可以进行位移提交,如果可以,那么就会提交上一次轮询的位移;
  • 自动位移提交仍会带来重复消费消息丢失现象;

手动提交

手动提交分为:同步提交异步提交

  1. 同步提交
  • 对应KafkaConsumer中的commitSync()方法;
  • commitSync()提交消费位移时,会阻塞消费者线程直至位移提交完成;
/**
 * KafkaConsumer 同步位移提交方法 commitSync
 */
public void commitSync();
public void commitSync(Duration timeout);
public void commitSync(final Map offsets);
public void commitSync(final Map offsets, final Duration timeout);
  1. 异步提交
  • 对应KafkaConsumer中的commitAsync()方法;
  • commitAsync()执行时,消费者线程不会被阻塞,可能在提交消费位移的结果还未返回之前就开始了新一次的拉取操作;
  • 异步提交可以使消费者的性能得到一定的增强;
/**
 * KafkaConsumer 异步位移提交方法 commitAsync
 */
public void commitAsync();
public void commitAsync(OffsetCommitCallback callback);
public void commitAsync(final Map offsets, OffsetCommitCallback callback);
  • commitAsync()中可以传递一个异步提交的回调方法,OffsetCommitCallback接口中有一个方法onComplete(),在提交完成时调用
public interface OffsetCommitCallback {
    void onComplete(Map offsets, Exception exception);
}

8. 反序列化器

生产者KafkaProducer发送消息时会调用序列化器将消息转换成字节数组byte[],消费者KafkaConsumer在接收消息时,会调用对应的反序列化器将字节数组反序列化为消息对象;

反序列化器需要实现接口Deserializer:

/**
 * 反序列化器接口
 */
public interface Deserializer extends Closeable {

    void configure(Map configs, boolean isKey);

    T deserialize(String topic, byte[] data);

    @Override
    void close();
}
  • configure()方法用来配置当前类;
  • deserialize()方法用来执行反序列化,如果data为null,那么处理的时候直接返回null,而不是抛出一个异常;
  • void() 用来关闭当前序列化器;

Kafka提供的反序列化器有:ByteBufferDeserializerByteArrayDeserializerBytesDeserializerDoubleDeserializerFloatDeserializerIntegerDeserializerLongDeserializerShortDeserializerStringDeserializer,分别用于ByteBufferByteArrayBytesDoubleFloatIntegerLongShortString类型的反序列化;

8.1 自定义反序列化器

如果Kafka提供的反序列化器满足不了需求时,可以自定义实现反序列化器,推荐使用通用的序列化工具,如JSONProtoBufProtostuff等,并且自定义的反序列化器需要与序列化器配套;

public class ProtoStuffDeserializer implements Deserializer {
    @Override
    public void configure(Map configs, boolean isKey) {

    }

    @Override
    public Object deserialize(String topic, byte[] data) {
        Schema schema;
        String result="";
        try {
            schema = RuntimeSchema.createFrom(result.getClass());
            ProtostuffIOUtil.mergeFrom(data, result, schema);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
        return result;
    }

    @Override
    public void close() {

    }
}
  • 要使用自定义的反序列化器,需要在key.deserializervalue.deserializer参数中指定使用的类;

9. 拦截器

  • 生产者在发送消息时,可以使用生产者拦截器在消息发送前发送回调逻辑前做一些定制化的需求;
  • 消息者也可以在消息消息时,使用消费者拦截器在消费到消息时提交消费位移之后做一些定制化需求;
  • 生产者拦截器的接口是ProducerInterceptor,消费者拦截器的接口是ConsumerInterceptor
/**
 * 消费者拦截器接口 ConsumerInterceptor
 */
public interface ConsumerInterceptor extends Configurable {
    public ConsumerRecords onConsume(ConsumerRecords records);
    
    public void onCommit(Map offsets);
    
    public void close();
}
  • onConsume()方法:KafkaConsumer会在poll()方法返回之前调用onConsume()方法来对消息进行定制化操作,如果onConsume()方法中抛出异常,那么会被捕获并记录到日志中,但是异常不会再向上传递;
  • onCommit()方法:KafkaConsumer会在提交完消费位移之后调用拦截器的onCommit()方法;
  • 要使自定义的消费者拦截器生效,需要在参数interceptor.classes参数中配置,该参数的默认值为"",即默认不使用消费组拦截器;

10. 多线程

生产者KafkaProducer是线程安全的,然而消费者KafkaConsumer是非线程安全的,KafkaConsumer中定义了一个acquire()方法,用来检测当前是否只有一个线程在操作,若有其他线程正在操作则抛出ConcurrentModifcationException异常;

KafkaConsumer中的每个公用方法(public方法),在执行所要执行的动作之前都会调用这个acquire()方法,只有wakeup()方法是个例外;

acquire()方法可通常所说的锁(synchronizedLock等)不同,它不会造成阻塞等待,仅通过线程操作计数标记的方式来检测线程是否发生了并发操作,以此保证只有一个线程在操作;

参考文献

  • 《深入理解Kafka核心设计与实践原理》 朱忠华 著,电子工业出版社.

你可能感兴趣的:(Kafka,kafka)