Kafka应用手册

142 阅读4分钟

本章将介绍Kafka的基础应用,并提供相关使用示例,包括Kafka脚本、客户端、配额等内容,以帮助读者掌握Kafka的使用方式。

3.1 脚本

前面我们使用zookeeper-server-start.sh脚本启动ZooKeeper服务,除了该脚本外,Kafka在安装包bin目录中还提供了很多脚本,帮忙我们更好地使用Kafka。

3.1.1 主题管理

(1)使用kafka-topics.sh创建主题。

$ ./bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 2 --partitions 3 --topic hello-topic
Created topic hello-topic.

该命令创建了一个新主题,新主题存在3个分区,每个分区存在2个副本。kafka-topics.sh提供了以下参数:

  • --bootstrap-server:Kafka Broker列表,这里只需要列举Kafka集群的部分节点信息,Kafka脚本(或者客户端)会自动查找Kafka集群所有节点信息。

  • --create:创建主题。

  • --partitions:分区数量,该主题会划分为多少个分区。

  • --replication-factor:副本数量,每个分区总共有多少个副本。该数量需要小于或等于Broker的数量。

  • --replica-assignment:指定副本分配方案,不能与—partitions和--replication- factor参数同时使用(第7章详细介绍该参数)。

  • --alter:变更主题,如修改分区数量、AR副本列表等。

  • --config :设置主题的配置项,Kafka中可以针对主题设置配置,如cleanup.policy、compression.type、delete.retention.ms等,这些配置项会在后面内容中介绍。

  • --delete:删除主题。

  • --list:列举有效的主题。

  • --describe:查询主题详细信息。

提示:从Kafka 3.0开始,很多Kafka脚本都已经移除了zookeeper参数。

(2)使用以下命令查看主题详细信息。

$./bin/kafka-topics.sh --describe --bootstrap-server localhost:9092 --topic hello-topic
Topic: hello-topic TopicId: PXL26BtdQsWxFJSi3glVag PartitionCount: 3 ReplicationFactor: 2 Configs: segment.bytes=1073741824
Topic: hello-topic    Partition: 0    Leader: 2    Replicas: 2,0    Isr: 2,0
Topic: hello-topic    Partition: 1    Leader: 1    Replicas: 1,2    Isr: 1,2
Topic: hello-topic    Partition: 2    Leader: 0    Replicas: 0,1    Isr: 0,1

该命令会输出主题每个分区的leader副本、AR副本列表、ISR等信息。

3.1.2 生产者与消费者

(1)使用kafka-console-producer.sh脚本可以给指定主题发送消息。

$ ./bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic hello-topic
> hello kafka(输入消息内容,回车发送)

(2)使用kafka-console-consumer.sh脚本可以消费指定主题的消息。

$ ./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic hello-topic --group hello-group --from-beginning
hello-kafka(收到消息)
  • --group:指定消费者所在消费组,如果不指定,则脚本自动为我们指定一个消费组:console-consumer-<随机数>,随机数小于100000。

kafka-console-producer.sh、kafka-console-consumer.sh同样提供了很多参数,这些参数在3.2小节会进行详细说明。

(3)使用kafka-consumer-groups.sh脚本可以管理消费组,如查询消费组列表。

$ ./bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list
hello-group

使用以下命令可以查看指定消费组的详细信息:

$ ./bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group
GROUP  TOPIC   PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG ...
my-group   my-topic   0    917    924    7    ...
my-group   my-topic   1    0      0      0    ...
my-group   my-topic   2    0      0      0    ...

该命令会输出指定消费组订阅的所有主题、分区、最新消费偏移量(CURRENT-OFFSET)、分区最新偏移量(LOG-END-OFFSET)、待消费数量(LAG)等信息。我们可以通过待消费数量(LAG)判断消费者是否消费滞后:消费者消费速度跟不上生产者生产速度。从分区最新偏移量(LOG-END-OFFSET)可以看到,hello-topic可能存在数据不均衡的问题,大量的数据存储在Partition0中。kafka-consumer-groups.sh脚本还支持以下关键参数:

  • --state:查看消费组状态。

  • --members:查看消费组成员。

  • --offsets:查询ACK偏移量。

  • --reset-offsets:重置消费组的ACK偏移量。

3.1.3 动态配置

前面我们使用Kafka安装目录下的config/server.properties配置文件启动Kafka服务,该配置文件可以设置Broker配置项。如果我们要修改Kafka相关配置,那么必须修改该配置文件并重启Kafka服务。在生产环境中,并不能随便重启Kafka服务,所以Kafka提供了动态配置机制,允许我们在Kafka运行时修改配置项,而Broker服务会在运行时定时拉取这些动态配置项的最新值。

使用kafka-configs.sh脚本可以管理动态配置。该脚本可以针对Kafka的主题、Broker、用户、客户端等对象设置动态配置。

提示:可以执行./bin/kafka-configs.sh --help命令查看kafka-configs.sh的使用方式及支持的动态配置。

(1)对指定主题添加动态配置。

$ ./bin/kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic hello-topic --add-config 'max.message.bytes=1048576'

对指定Broker节点添加动态配置:

$ ./bin/kafka-configs.sh --bootstrap-server localhost:9092 --alter --broker 0 --add-config 'log.segment.bytes=788888888'

(2)查看指定主题的动态配置。

$ ./bin/kafka-configs.sh --bootstrap-server localhost:9092 --describe   --topic hello-topic

查看指定Broker节点的动态配置:

$ ./bin/kafka-configs.sh --bootstrap-server localhost:9092 --describe   --broker 0

(3)kafka-configs.sh脚本也可以设置默认的动态配置。例如,为所有的Broker节点设置一个默认的动态配置。

$ ./bin/kafka-configs.sh --bootstrap-server localhost:9092 --alter --broker-defaults --add-config 'log.segment.bytes=788888888'

使用以下命令可以查看默认动态配置:

$ ./bin/kafka-configs.sh --bootstrap-server localhost:9092 --describe --broker-defaults

那么如果在Broker配置文件、默认动态配置、指定BrokerId的动态配置中,都存在同一个配置项,哪个配置项的优先级更高呢?配置优先级为:Broker配置文件<默认动态配置<指定BrokerId的动态配置

(4)使用以下命令删除动态配置。

$ ./bin/kafka-configs.sh --bootstrap-server localhost:9092 --alter --broker 0 --delete-config 'log.segment.bytes'

Kafka还提供了其他脚本,如kafka-producer-perf-test.sh与kafka-consumer-perf-test.sh脚本可以进行性能测试,kafka-get-offsets.sh脚本可以获取ACK偏移量。本书就不一一介绍了,读者可以自行了解。

3.2 客户端

Kafka官方提供了Java客户端,用于发送、接收消息。下面介绍如何使用Kafka的客户端。

3.2.1 生产者

1.生产者示例(1)添加Kafka客户端的Maven引用。

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

(2)编写一个简单的Kafka生产者。

public class BasicProducer {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Properties props = new Properties();
        props.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
        props.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // 【1】
        KafkaProducer producer = new KafkaProducer<String, String>(props);
        // 【2】
        List<Future<RecordMetadata>> futureList = new ArrayList<>();
        for(int i = 0; i < 10; i++) {
            Future<RecordMetadata> future = producer.send(
                new ProducerRecord<String, String>("hello-topic", String.valueOf(i), "message-" + i));
            futureList.add(future);
        }
        // 【3】
        for (Future<RecordMetadata> future : futureList) {
            System.out.println("send:" + future);
        }
    }
}

【1】构建生产者KafkaProducer,并使用Properties指定生产者配置项,如bootstrap.servers等。

【2】调用KafkaProducer#send方法发送消息。该方法异步发送消息,返回一个Future结果。

【3】打印Future结果,这里异步发送了所有消息后再一起打印结果,这样才能使用生产者批量发送机制。2.消息属性Kafka为消息定义了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;
    ...
}

可以看到,Kafka消息中包含了主题(topic)、分区(partition)、消息头(header,可以携带额外的键值对,默认为空)、消息键(key)、消息值(value)、时间戳(timestamp)等信息。

提示:Kafka中也会将消息称为记录(record)。

3.生产者配置上面的例子中仅使用了基础的生产者配置项,Kafka提供了很多配置项,这些配置项可以帮助我们更好地使用客户端。常用的配置项如下:

  • bootstrap.servers:Broker集群地址,只需要配置集群中的部分节点即可。

  • key.serializer、value.serializer:消息键、值的序列化器。

  • client.id:作为客户端标志,发送给Broker,用于跟踪请求源、在配额机制中标识客户端等场景。

  • ack:生产者和Broker(客户端会将该参数发送给Broker)判断消息是否写入成功的策略,有如下取值。

0:生产者正常发送消息后不需要等待任何Broker响应,就可以判定消息写入成功。这样可以以最大速度发送消息,但可能丢失消息(由于网络阻塞、Broker故障等原因)。

 1:只要leader副本收到并保存消息,就会返回成功响应给生产者,生产者收到响应后就会认为该消息写入成功。如果在follow副本还没有同步这些消息前,leader副本就因故障下线了,则这些消息将丢失。

 -1:也可以配置为all,leader副本收到消息后,需要等待ISR(已同步副本)中所有副本都同步消息后,才返回成功响应给生产者,这种模式最安全。

  • batch.size:消息批次大小上限,默认值为16384(16KB)。生产者会将消息缓存到消息批次(batch)中,直到批次大小达到该配置值,再将消息批次发送给Broker。

  • linger.ms:消息延迟时间上限。如果消息批次中消息缓存时间达到该配置值,生产者就会发送该消息批次(即使消息批次大小未达到上限),默认值为0。批量发送消息减少了网络通信次数,并且有助于Broker利用磁盘顺序读写机制提高I/O效率,从而提高Kafka性能,所以可以根据实际情况(生产者内存大小、消息及时性要求)适当增大batch.size、linger.ms的配置值。

  • buffer.memory:生产者整个缓冲区(即所有消息批次)的大小上限,默认值为33554432(32MB)。

  • max.request.size:单个消息的大小上限,默认值为1048576(1MB)。

  • compression.type:生产者生成的所有数据的压缩类型。默认值为none(即无压缩),有效值为none、gzip、snappy、lz4 或 zstd。

  • max.block.ms:指定生产者最长阻塞等待时间,默认值为60000(1分钟)。生产者发送消息或获取集群元数据时,如果当前缓冲区已满或没有可用的元数据,那么生产者将阻塞等待,该配置项指定了这两个操作最长的阻塞时间。

  • request.timeout.ms:指定客户端等待请求响应的最长等待时间,默认值为30000(30秒)。如果等待超时且没有收到响应,那么客户端将在必要时重新发送消息,如果重试次数用尽,则请求失败。

  • retries、retry.backoff.ms:retires指定消息发送失败后自动重发的次数,默认值为2147483647;retry.backoff.ms指定重发的时间间隔,默认值为100(ms)。

  • delivery.timeout.ms:发送消息的最长时间,即多次重试发送所允许的最长时间。超过该配置值指定时间且未成功发送的消息将发送失败,不允许再重试(即使重试次数没用完)。该配置值应该大于request.timeout.ms+linger.ms,默认值为120000(2分钟)。

  • max.in.flight.requests.per.connection:每个连接上等待响应的最大消息数量,默认值为5,即如果存在5个已发送但Broker未返回响应的消息,则不允许再发送新的消息。

  • metadata.max.idle.ms:集群元数据有效时间,默认值为300000(5分钟)。生产者会缓存集群元数据,当元数据缓存时间超过该配置值时,缓存将失效,生产者会获取新的元数据,以发现集群变化,如主题、Broker的变化。

  • enable.idempotence:是否启动生产者幂等机制,默认值为true,后面会详细分析。

  • receive.buffer.bytes、send.buffer.bytes:这两个配置项分别指定了TCP Socket接收缓冲区和发送缓冲区的大小,如果设置成-1,则使用操作系统的默认值。如果客户端与Broker 网络通信延迟比较高,则可以适当增大这两个配置项的值。

  • reconnect.backoff.ms:当连接失败时,尝试重新连接Broker之前的等待时间(也称退避时间),避免频繁地连接主机,默认值为50(ms)。

  • partitioner.class:指定生产者分区器实现类,该分区器负责将消息划分给不同的分区,Kafka提供了如下消息分区器:

RoundRobinPartitioner:轮询分区器,该分区器使用round-robin轮询的方式将消息指定给存活的分区。 

 UniformStickyPartitioner:黏性分区器,该分区器会尽可能地先向一个分区发送消息,将这个分区的缓冲区快速填满后再切换到下一个分区,这样可以降低消息的发送延迟。 DefaultPartitioner:默认分区器,使用消息键的Hash值对主题分区数量取模,得到该消息的分区。如果消息键为空,则使用同UniformStickyPartitioner一样的策略,集中向一个分区发送键为空的消息,让这个分区的缓冲区快速填满。

用户也可以实现org.apache.kafka.clients.producer.Partitioner接口来自定义消息分区器,并通过生产者配置项partitioner.class指定自定义的消息分区器。

4.生产者拦截器Kafka提供了ProducerInterceptor接口,用于实现生产者拦截器,在生产者执行某些操作时添加额外的处理逻辑。ProducerInterceptor接口提供了以下方法:

  • onSend:发送消息前触发该方法,通过该方法可以修改发送消息的内容。

  • onAcknowledgement:生产者收到Broker返回响应(成功或者异常)时触发该方法。

  • close:拦截器被关闭时触发该方法(关闭生产者时会关闭拦截器),可以执行一些清理资源的工作。

下面展示一个生产者拦截器的使用示例。

实现ProducerInterceptor接口,统计消息发送数量:

public class CounterInterceptor implements ProducerInterceptor<String, String> {
    private AtomicInteger sendCount = new AtomicInteger(0);
​
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        System.out.println("send count:" + sendCount.incrementAndGet());
        return record;
    }
    ...
}

(2)通过生产者配置interceptor.classes,将拦截器注册到生产者中。

Properties props = new Properties();
...
List<String> interceptors = new ArrayList<>();
interceptors.add(CounterInterceptor.class.getName());
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
​
KafkaProducer producer = new KafkaProducer<String, String>(props);

这样就可以使用拦截器了。生产者配置interceptor.classes指定了一个拦截器列表,生产者会按照该列表中的顺序执行其中的拦截器。

3.2.2 消费者

1.消费者示例

(1)添加Kafka客户端的Maven引用,与生产者相同。

(2)编写一个简单的Kafka消费者。

public class BasicConsumer {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
​
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "hello-group");
​
        // 【1】
        KafkaConsumer consumer = new KafkaConsumer<String, String>(props);
        // 【2】
        consumer.subscribe(Arrays.asList("hello-topic"));
​
        // 【3】
        for(;;) {
            ConsumerRecords<String, String> records = consumer.poll (Duration.ofSeconds(5));
            // 【4】
            for (ConsumerRecord<String, String> record : records) {
                System.out.println("receive:" + record);
            }
            
        }
    }
}

【1】创建消费者KafkaConsumer,并使用Properties指定配置项,如group.id等。

【2】调用KafkaConsumer#subscribe方法订阅指定主题。

【3】调用KafkaConsumer#poll方法拉取数据,该方法的参数指定请求的最长阻塞时间。Kafka消费者使用“拉模式”拉取消息。

【4】处理拉取到的消息。

2.消费者配置下面介绍Kafka消费者常用的配置项。

  • group.id:消费组名称。

  • enable.auto.commit:消费者是否自动提交ACK偏移量,默认值为true。

  • auto.commit.interval.ms:指定提交ACK偏移量的时间间隔,默认值为5000(5秒)。

  • auto.offset.reset:如果该消费组是新的消费组或者消费组ACK偏移量已经被清除,指定消费者从哪里开始消费,存在以下选项: > earliest:从最早的偏移量开始消费,即消费所有的历史消息。 > latest:从最近的偏移量开始消费,只消费新的消息,默认值。 > none:抛出异常。

  • fetch.max.bytes:每次拉取数据的最大数据量,默认值为52428800(50MB)。

  • max.partition.fetch.bytes:一个分区中每次拉取数据的最大数据量,默认值为1048576(1MB)。

  • fetch.min.bytes:每次拉取数据的最小数据量,消费者会要求Broker聚集消息,直到消息数据量达到该配置值后再将数据返回给消费者,默认值为1(字节)。

  • fetch.max.wait.ms:拉取数据的最大阻塞时间。如果拉取数据时阻塞时间达到该配置值,Broker立刻返回数据(即使数据量达不到fetch.min.bytes),默认值为500(ms)。

  • max.poll.records:每次拉取消息的最大数量,默认值为500。

  • session.timeout.ms:消费者超时时间,如果Broker超过该时间没有收到消费者心跳,则认为该消费者因故障而失效,默认值为45000(45秒)。

  • max.poll.interval.ms:指定两次拉取数据的最大时间间隔,默认值为300000(5分钟),注意消费者每次通过调用Consumer#poll方法拉取到的消息需要在该配置项指定的时间内处理完成,并及时调用下一次Consumer#poll方法,否则Kafka认为该消费者出现故障,并执行消费者重平衡操作,这样会影响消费者性能。

  • metadata.max.age.ms:自动刷新集群元数据的时间间隔,默认值为300000(5分钟)。

  • heartbeat.interval.ms:心跳发送时间间隔,默认值为3000(3秒)。消费者需要定时发送心跳给Broker,用于保持会话处于活动状态。注意该配置值必须小于session.timeout.ms,通常需要小于session.timeout.ms配置项的1/3。

  • partition.assignment.strategy:消费者分区分配策略,指定将分区分配给消费者的具体策略,默认值为RangeAssignor。这是一个重要的配置项,在第9章将详细介绍。

消费者同样支持receive.buffer.bytes、send.buffer.bytes、request.timeout.ms、connections.max. idle.ms等配置项,作用与生产者一样,不再赘述。

3. ACK提交方式在上面的例子中,我们使用了自动提交ACK的方式,由消费者定时将最新已处理成功的ACK偏移量发送给Broker。我们也可以使用手动提交ACK的方式,自行将ACK偏移量提交给Broker。

将上面的消费者例子调整为如下代码:

// 【1】
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
​
KafkaConsumer consumer = new KafkaConsumer<String, String>(props);
consumer.subscribe(Arrays.asList("hello-topic"));
​
for(;;) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(5));
    for (ConsumerRecord<String, String> record : records) {
        ...
    }
    // 【2】
    consumer.commitAsync();
}

【1】关闭消费者自动提交ACK偏移量的机制。

【2】处理消息后,调用Consumer#commitAsync方法提交ACK偏移量。

4.正则表达式订阅在上面的例子中,我们使用主题名集合订阅主题。我们也可以使用正则表达式订阅主题:所有主题名匹配该正则表达式模式的主题都可以被订阅。

Pattern pattern = Pattern.compile(".+-topic");
consumer.subscribe(pattern);

以上示例就可以同时订阅my-topic、user-topic两个主题。消费者会定时更新Kafka元数据(由配置项metadata.max.age.ms指定时间间隔),如果Kafka中创建了新的匹配正则表达式的主题,则消费者更新元数据后也可以订阅。相关配置:

  • exclude.internal.topics:使用正则表达式订阅时是否过滤Kafka内部主题。Kafka定义了两个内部主题:_consumer_offsets和_transaction_state,分别用于存储ACK偏移量和Kafka事务信息,该配置项的默认值为true,即不能使用subscribe(Pattern)的方式来订阅内部主题(可以使用subscribe(Collection)的方式)。

5.消费定位Kafka提供了KafkaConsumer#seek方法,可以让消费者从指定位置开始消费:

consumer.seek(new TopicPartition("hello-topic", 0), 10);

上述代码会将该消费者在"hello-topic"主题0分区的消费位置定位到偏移量10,该消费者将重新消费偏移量为10的消息及其后续的消息。

由于Kafka并没有提供定位到指定时间的方法,因此如果我们需要定位到某个时间点,那么可以使用如下方式:

// 【1】
consumer.subscribe(Arrays.asList("hello-topic"),  new ConsumerRebalanceListener() {
    public void onPartitionsAssigned(Collection<TopicPartition> collection) {
        // 【2】
        Map<TopicPartition, Long> timestampsToSearch = new HashMap<>();
        timestampsToSearch.put(new TopicPartition("hello-topic", 0),
                System.currentTimeMillis() - 1000 * 60);
        Map<TopicPartition, OffsetAndTimestamp> offsetMap = 
          consumer.offsetsForTimes (timestampsToSearch);
        // 【3】
        offsetMap.forEach((tp, OffsetAndTime) -> {
            if(OffsetAndTime != null) {
                consumer.seek(tp, OffsetAndTime.offset());
            }
        });
    }
    ...
});

【1】订阅hello-topic主题并注册ConsumerRebalanceListener回调类,当订阅成功后再执行【2】、【3】步骤,重定位消费位置。注意:消费者每次重平衡都会触发ConsumerRebalanceListener中的回调方法。

【2】调用Consumer#offsetsForTimes获取“hello-topic”主题0分区1分钟前的偏移量。

【3】调用Consumer#seek将消费者消费位置定位到1分钟前。

6.消费者拦截器Kafka消费者同样支持拦截器,只要实现ConsumerInterceptor就可以。ConsumerInterceptor提供了3个方法:

  • onConsume:在Broker返回消息后,并且消息返回到KafkaConsumer#poll前触发该方法。

  • onCommit:当提交ACK偏移量时触发该方法。

  • close:关闭拦截器时触发该方法。

使用消费者配置interceptor.classes可以将拦截器注册到消费者中。与生产者类似,不再赘述。

3.3 消息序列化

我们可以指定消息键、值的序列化器,Kafka还提供了String、Long、Integer、Float、Double、Byte的序列化器。另外,读者也可以实现org.apache.kafka.common.serialization.Serializer、org.apache.kafka.common.serialization.Deserializer两个接口,自定义序列化器、反序列化器。

(1)下面使用Jackson实现一个简单的JSON序列化器,并使用该JSON序列化器序列化消息。

public class JsonSerializer<T>  implements Serializer<T>{
    private static ObjectMapper objectMapper = new ObjectMapper();
​
    public byte[] serialize(String topic, T data) {
        try {
            return objectMapper.writeValueAsBytes(data);
        } ...
        return null;
    }
}

该JSON序列化器的实现很简单,要使用该JSON序列化器也很简单,使用生产者配置项value.serializer指定该序列化器即可。

props.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class.getName());
...
KafkaProducer producer = new KafkaProducer<String, User>(props);

这样就可以直接使用Bean实例作为消息值,生产者会使用JSON序列化器序列化Bean实例:

User u = new User();
Future<RecordMetadata> future = producer.send(new ProducerRecord<Long, User>("user-topic", null , u));

(2)我们也可以使用Jackson实现反序列化器。

public class JsonDeserializer<T> implements Deserializer<T> {
    private static ObjectMapper objectMapper = new ObjectMapper();
    private Class targetClass;
​
    // 【1】
    public void configure(Map<String, ?> configs, boolean isKey) {
        Object clazz;
        if(isKey) {
            clazz = configs.get("key.deserializer.class");
        } else {
            clazz = configs.get("value.deserializer.class");
        }
​
        targetClass = (Class) clazz;
    }
​
    public T deserialize(String topic, byte[] data) {
        try {
            // 【2】
            return (T) objectMapper.readValue(data, targetClass);
        } ...
        return null;
    }
}

【1】configs参数就是构建KafkaConsumer时传入的Properties参数,这里从该参数中读取反序列化的目标类。笔者定义了两个配置项:key.deserializer.class、value.deserializer.class,用于在创建消费者时指定反序列化的目标类。

【2】使用Jackson反序列化数据。下面指定反序列化器和反序列化目标类:

props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class.getName());
props.put("value.deserializer.class", User.class);
...
​
KafkaConsumer consumer = new KafkaConsumer<String, User>(props);

这样消费者就可以直接接收JSON格式的数据,并将其转换为对应的Bean实例。

for(;;) {
    ConsumerRecords<Long, User> records = consumer.poll(Duration.ofSeconds(5));
    for (ConsumerRecord<Long, User> record : records) {
        System.out.println("receive:" + record.value());
    }
}

3.4 配额

某些生产者和消费者可能会快速生产/消费大量消息,从而垄断Kafka集群资源,导致其他生产者和消费者无法正常工作。这时可以使用配额机制对这些生产者和消费者限流,避免它们过度占用资源。

如果要对客户端限流,那么首先需要明确客户端的身份。客户端可以使用两种方式声明自己的身份:

(1)使用clientId属性声明身份(多个客户端可以使用同一个client-id)。

(2)通过认证机制声明用户身份。

下面介绍如何使用Kafka的配额机制。

(1)使用以下命令,给指定clientId的客户端添加配额配置。

$ bin/kafka-configs.sh --alter --bootstrap-server localhost:9092 --add-config 'producer_byte_rate=1024,consumer_byte_rate=1024' --entity-type clients --entity-name clientA
  • producer_byte_rate:生产者发送消息速率不能超过该阈值,单位为字节/秒。

  • consumer_byte_rate:消费者接收消息速率不能超过该阈值,单位为字节/秒。

客户端使用client-id声明身份:

props.setProperty(ProducerConfig.CLIENT_ID_CONFIG, "clientA");

这样该客户端发送和接收消息将受到配额的限制,每秒只能发送或者接收1KB的消息。

(2)使用以下命令可以给指定认证身份的客户端添加配额配置。

$ bin/kafka-configs.sh --bootstrap-server localhost:9092 --alter --add-config 'producer_byte_rate=1024,consumer_byte_rate=2048,request_percentage=200' --entity-type users --entity-name alice

这时,使用alice用户认证的客户端将受到配额的限制。认证机制将在第22章介绍。

3.5 本章总结

本章介绍了Kafka的使用方式,包括管理脚本、生产者与消费者的使用方式和关键配置,消息序列化、配额机制的使用等内容。这部分内容可以帮助读者了解Kafka提供的功能及日常如何使用Kafka。

对了,五一开展的抽奖送书活动就剩下最后一周了,感兴趣的朋友可以关注公众号binecy,发送“抽奖Redis”或“抽奖Kafka/Pulsar”,即可抽奖笔者新书《Redis核心原理与实践》《深入理解Kafka与Pulsar》,谢谢大家支持。