4.2.2 Kafka高级特性解析(生产者消息发送、序列化器拦截器;消费者消息接收、反序列化、位移提交、再均衡、消费组)

Kafka高级特性解析



2.1 生产者

2.1.1 消息发送

2.1.1.1 数据生产流程解析

在这里插入图片描述

  1. Producer创建时,会创建一个Sender线程并设置为守护线程。
  2. 生产消息时,内部其实是异步流程;生产的消息先经过拦截器->序列化器->分区器,然后将消
    息缓存在缓冲区(该缓冲区也是在Producer创建时创建)。
  3. 批次发送的条件为:缓冲区数据大小达到batch.size或者linger.ms达到上限,哪个先达到就算
    哪个。
  4. 批次发送后,发往指定分区,然后落盘到broker;如果生产者配置了retrires参数大于0并且
    失败原因允许重试,那么客户端内部会对该消息进行重试。
  5. 落盘到broker成功,返回生产元数据给生产者。
  6. 元数据返回有两种方式:一种是通过阻塞直接返回,另一种是通过回调返回。

2.1.1.2 必要参数配置

broker配置
  1. 配置条目的使用方式:
    在这里插入图片描述
  2. 配置参数:
    在这里插入图片描述

2.1.1.3 序列化器

在这里插入图片描述
由于Kafka中的数据都是字节数组,在将消息发送到Kafka之前需要先将数据序列化为字节数组。
序列化器的作用就是用于序列化要发送的消息的。

Kafka使用org.apache.kafka.common.serialization.Serializer 接口用于定义序列化器,将泛型指定类型的数据转换为字节数组。

package org.apache.kafka.common.serialization;
 
import java.io.Closeable;
import java.util.Map;
import org.apache.kafka.common.header.Headers;
 
/**
  * 将对象转换为byte数组的接口
  *
  * 该接口的实现类需要提供无参构造器
  * @param <T> 从哪个类型转换
  */
public interface Serializer<T> extends Closeable {
   
 
 /**
   * 类的配置信息
   * @param configs key/value pairs
   * @param isKey key的序列化还是value的序列化
  */
    default void configure(Map<String, ?> configs, boolean isKey) {
   
    }
 
/**
  * 将对象转换为字节数组
  *
  * @param topic 主题名称
  * @param data 需要转换的对象
  * @return 序列化的字节数组
  */
    byte[] serialize(String var1, T var2);
 
 
    default byte[] serialize(String topic, Headers headers, T data) {
   
        return this.serialize(topic, data);
    }
 
  /**
    * 关闭序列化器
    * 该方法需要提供幂等性,因为可能调用多次。
    */    
    default void close() {
   
    }
}

系统提供了该接口的子接口以及实现类:
org.apache.kafka.common.serialization.ByteArraySerializer
在这里插入图片描述
org.apache.kafka.common.serialization.ByteBufferSerializer
在这里插入图片描述
org.apache.kafka.common.serialization.BytesSerializer
在这里插入图片描述
org.apache.kafka.common.serialization.DoubleSerializer
在这里插入图片描述
org.apache.kafka.common.serialization.FloatSerializer
在这里插入图片描述
org.apache.kafka.common.serialization.IntegerSerializer
在这里插入图片描述
org.apache.kafka.common.serialization.StringSerializer
在这里插入图片描述
org.apache.kafka.common.serialization.LongSerializer
在这里插入图片描述
org.apache.kafka.common.serialization.ShortSerializer
在这里插入图片描述

自定义序列化器

数据的序列化一般生产中使用avro。
自定义序列化器需要实现org.apache.kafka.common.serialization.Serializer接口,并实现其中的 serialize 方法。
案例:
实体类:

package com.lagou.kafka.demo.entity;

public class User {
   
    private Integer userId;
    private String username;

    public Integer getUserId() {
   
        return userId;
    }

    public void setUserId(Integer userId) {
   
        this.userId = userId;
    }

    public String getUsername() {
   
        return username;
    }

    public void setUsername(String username) {
   
        this.username = username;
    }
}

序列化类:

package com.lagou.kafka.demo.serializer;

import com.lagou.kafka.demo.entity.User;

import org.apache.kafka.common.errors.SerializationException;
import org.apache.kafka.common.serialization.Serializer;

import java.io.UnsupportedEncodingException;

import java.nio.Buffer;
import java.nio.ByteBuffer;

import java.util.Map;


public class UserSerializer implements Serializer<User> {
   
    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
   
        // do nothing
        // 用于接收对序列化器的配置参数,并对当前序列化器进行配置和初始化的
    }

    @Override
    public byte[] serialize(String topic, User data) {
   
        try {
   
            // 如果数据是null,则返回null
            if (data == null) {
   
                return null;
            }

            Integer userId = data.getUserId();
            String username = data.getUsername();
            int length = 0;
            byte[] bytes = null;

            if (null != username) {
   
                bytes = username.getBytes("utf-8");
                length = bytes.length;
            }

			// 第一个4个字节用于存储userId的值
            // 第二个4个字节用于存储username字节数组的长度int值
            // 第三个长度,用于存放username序列化之后的字节数组
            ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + length);
            buffer.putInt(userId);
            buffer.putInt(length);
            buffer.put(bytes);

			// 以字节数组形式返回user对象的值
            return buffer.array();
        } catch (UnsupportedEncodingException e) {
   
            throw new SerializationException("序列化数据异常");
        }
    }

    @Override
    public void close() {
   
        // do nothing
        // 用于关闭资源等操作。需要幂等,即多次调用,效果是一样的。
    }
}

生产者:

package com.lagou.kafka.demo.producer;
 
import com.lagou.kafka.demo.entity.User;
import com.lagou.kafka.demo.serialization.UserSerializer;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
 
import java.util.HashMap;
import java.util.Map;
 
public class MyProducer {
   
    public static void main(String[] args) {
   
 
        Map<String, Object> configs = new HashMap<>();
        configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092");
        configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 设置自定义的序列化器
        configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, UserSerializer.class);
 
        KafkaProducer<String, User> producer = new KafkaProducer<String, User>(configs);
 
        User user = new User();
        user.setUserId(1001);
        user.setUsername("赵四");
 
        ProducerRecord<String, User> record = new ProducerRecord<String, User>(
                "tp_user_01",   // topic , 没有会自动创建
                user.getUsername(),   // key , 根据key值放到不同分区
                user                  // value
        );
 
        // 异步确认机制
        producer.send(record, new Callback() {
   
            @Override
            public void onCompletion(RecordMetadata metadata, Exception exception) {
   
                if (exception != null) {
   
                    System.out.println("消息发送异常");
                } else {
   
                    System.out.println("主题:" + metadata.topic() + "\t"
                    + "分区:" + metadata.partition() + "\t"
                    + "生产者偏移量:" + metadata.offset());
                }
            }
        });
 
        // 关闭生产者
        producer.close();
    }
}

2.1.1.4 分区器

在这里插入图片描述
默认(DefaultPartitioner)分区计算:

  1. 如果record提供了分区号,则使用record提供的分区号
  2. 如果record没有提供分区号,则使用key的序列化后的值的hash值对分区数量取模
  3. 如果record没有提供分区号,也没有提供key,则使用轮询的方式分配分区号。
    a. 会首先在可用的分区中分配分区号
    b. 如果没有可用的分区,则在该主题所有分区中分配分区号。

在这里插入图片描述
在这里插入图片描述
如果要自定义分区器,则需要

  1. 首先开发Partitioner接口的实现类
  2. 在KafkaProducer中进行设置:configs.put(“partitioner.class”, “xxx.xx.Xxx.class”)

位于 org.apache.kafka.clients.producer 中的分区器接口:

package org.apache.kafka.clients.producer;
import org.apache.kafka.common.Configurable;
import org.apache.kafka.common.Cluster;
import java.io.Closeable;
/**
* 分区器接口
*/
public interface Partitioner extends Configurable, Closeable {
   
 
  /**
  * 为指定的消息记录计算分区值
  *
  * @param topic 主题名称
  * @param key 根据该key的值进行分区计算,如果没有则为null。
  * @param keyBytes key的序列化字节数组,根据该数组进行分区计算。如果没有key,则为null
  * @param value 根据value值进行分区计算,如果没有,则为null
  * @param valueBytes value的序列化字节数组,根据此值进行分区计算。如果没有,则为null
  * @param cluster 当前集群的元数据
  */
  public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
  
 
  /**
  * 关闭分区器的时候调用该方法
  */
  public void close();
}

包 org.apache.kafka.clients.producer.internals 中分区器的默认实现:

package org.apache.kafka.clients.producer.internals;
 
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.utils.Utils;
 
/**
* 默认的分区策略:
*
* 如果在记录中指定了分区,则使用指定的分区
* 如果没有指定分区,但是有key的值,则使用key值的散列值计算分区
* 如果没有指定分区也没有key的值,则使用轮询的方式选择一个分区
*/
public class DefaultPartitioner implements Partitioner {
   
 
  private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();
 
  public void configure(Map<String, ?> configs) {
   }
 
 
  /**
  * 为指定的消息记录计算分区值
  *
  * @param topic 主题名称
  * @param key 根据该key的值进行分区计算,如果没有则为null。
  * @param keyBytes key的序列化字节数组,根据该数组进行分区计算。如果没有key,则为null
  * @param value 根据value值进行分区计算,如果没有,则为null
  * @param valueBytes value的序列化字节数组,根据此值进行分区计算。如果没有,则为null
  * @param cluster 当前集群的元数据
  */
  public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
   
 
    // 获取指定主题的所有分区信息
    List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
    // 分区的数量
    int numPartitions = partitions.size();
    // 如果没有提供key
    if (keyBytes == null) {
   
      int nextValue = nextValue(topic);
      List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
      if (availablePartitions.size() > 0) {
   
        int part = Utils.toPositive(nextValue) % availablePartitions.size();
        return availablePartitions.get(part).partition();
     } else {
   
        // no partitions are available, give a non-available partition
        return Utils.toPositive(nextValue) % numPartitions;
     }
   } else {
   
      // hash the keyBytes to choose a partition
      // 如果有,就计算keyBytes的哈希值,然后对当前主题的个数取模
      return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
   }
 }
 
  private int nextValue(String topic) {
   
 
    // 防止并发引起数据混乱
    AtomicInteger counter = topicCounterMap.get(topic);
    if (null == counter) {
   
      counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
      AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter);
      if (currentCounter != null) {
   
        counter = currentCounter;
     }
   }
    return counter.getAndIncrement();
 }
  public void close() {
   }
}

在这里插入图片描述
可以实现Partitioner接口自定义分区器:
在这里插入图片描述
然后在生产者中配置:
在这里插入图片描述

2.1.1.5 拦截器

在这里插入图片描述
Producer拦截器(interceptor)和Consumer端Interceptor是在Kafka 0.10版本被引入的,主要用于实现Client端的定制化控制逻辑。
对于Producer而言,Interceptor使得用户在消息发送前以及Producer回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。同时,Producer允许用户指定多个Interceptor按序作用于同一条
消息从而形成一个拦截链(interceptor chain)。Intercetpor的实现接口是o

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值