参考:深入理解Kafka核心设计和实践原理
7、消息的消费:
消息的消费一般基于两种模式:push模式和pull模式,即主动推模式和主动拉模式。
推模式是服务端主动将消息推送给消费者,拉模式是消费者主动从服务端发起请求来拉取消息。
// 轮询拉取消息之前必须订阅topic 否则会报错。
@Override
public ConsumerRecords<K, V> poll(long timeout) {
acquire();
try {
if (timeout < 0)
throw new IllegalArgumentException("Timeout must not be negative");
if (this.subscriptions.hasNoSubscriptionOrUserAssignment())
throw new IllegalStateException("Consumer is not subscribed to any topics or assigned any partitions");
// poll for new data until the timeout expires 轮询新数据,直到超时结束
long start = time.milliseconds();
long remaining = timeout; // 消费位移的提交
do {
Map<TopicPartition, List<ConsumerRecord<K, V>>> records = pollOnce(remaining);
if (!records.isEmpty()) {
// 在返回消费的记录之前,我们可以发送下一轮提取,并避免在用户处理提取的消息时阻止等待其响应。注意:由于消费的的offset已经更新,因此在返回提取的记录之前,我们不能允许唤醒或触发任何其他错误
if (fetcher.sendFetches() > 0 || client.hasPendingRequests())
client.pollNoWakeup();
if (this.interceptors == null)
return ne**加粗样式**w ConsumerRecords<>(records);
else
return this.interceptors.onConsume(new ConsumerRecords<>(records));
}
long elapsed = time.milliseconds() - start;
remaining = timeout - elapsed;
} while (remaining > 0); // 提交消费位移
return ConsumerRecords.empty();
} finally {
release();
}
}
ConsumerRecord: 消费者消费到的每条消息的类型为ConsumerRecord。
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; // topic name
private final int partition; // partition id
private final long offset; // offset
private final long timestamp; // 时间戳
private final TimestampType timestampType; // 支持的时间戳类型
private final int serializedKeySize; // The length of the serialized key
private final int serializedValueSize; // The length of the serialized value
private final Headers headers; // The headers of the record.
private final K key; // The key of the record, if one exists (null is allowed) 消息的key 可为空
private final V value; // The record contents(消息内容)
private volatile Long checksum; // The checksum (CRC32) of the full record
/**
* 获取给定partition的记录
*/
public List<ConsumerRecord<K, V>> records(TopicPartition partition) {
List<ConsumerRecord<K, V>> recs = this.records.get(partition);
if (recs == null)
return Collections.emptyList();
else
return Collections.unmodifiableList(recs);
}
提交消费位移:指消费者消费到的位置
在旧消费者客户端中吗,消费位移是存储在ZooKeeper中的,而在新的消费者客户端中,消费位移存储在Kafka内部的主题__consumer_offsets中。这里把消费位移存储(持久化)的动作称为"提交",消费者在消费完消息之后需要执行消费位移的提交。
消费位移:表示下一次将要poll的消息的位置。
位移提交可能会造成重读消费或者消息丢失的问题。当前一次poll操作拉取的消息集为[1,6],1表示上一次提交的消费位移,表名已经完成了1之前的所有消息的消费,3表示当前处理的位置,如果拉取到消息后就进行位移的提交,即提交7,那么当前消费3的时候遇到了异常,在故障恢复之后,重新拉取消息是从7开始的,也就是说,3-6之间的消息未能被消费到,造成了消息丢失的问题。
还有一种情况,位移提交的动作实在消费完所有拉取到的消息之后才执行的,那么当前消费3的时候遇到了异常,故障恢复之后,重新拉取消息的位置是从1开始的,也就是说1-3之间的消息是重复被消费的,造成了消息重复消费的问题。
Kafka中消息位移的提交默认是自动的,即默认值enabe.auto.commit的默认值为true。此处需要说明的是,自动提交的额不是每消费一条消息就自动提交,而是定期提交的,默认值为5s。默认方式下,消费者每隔5s中会将拉取到的每个分区的最大消息位移进行提交。自动提交的动作实在poll方法中完成的。
指定位移消费
在Kafka中每当消费者找不到所记录的消费位移时,就会根据消息者客户端参数auto.offset.reset的配置参数决定从何处消费,这个参数的默认值为"latest",表示产品能从分区末尾即行消费,而另一个值配置值"earliest"表示从0开始消费。
KafkaConsumer 中的**seek()**方法可以追前消费或者回溯消费:
覆盖消费者将在下次轮询(超时)时使用的提取偏移量。如果多次为同一分区调用此API,则将在下一个poll()上使用最新的偏移量。请注意,如果此API在使用过程中被任意使用,您可能会丢失数据,以重置获取偏移量
@Override
public void seek(TopicPartition partition, long offset) {
if (offset < 0) {
throw new IllegalArgumentException("seek offset must not be a negative number");
}
acquire();
try {
log.debug("Seeking to offset {} for partition {}", offset, partition);
this.subscriptions.seek(partition, offset);
} finally {
release();
}
}
查找每个给定分区的第一个偏移量。此函数的计算比较缓慢,只在调用时查找所有分区中的第一个偏移量。如果没有提供分区,则查找当前分配的所有分区的第一个偏移量
seekToBeginning源码
public void seekToBeginning(Collection<TopicPartition> partitions) {
acquire();
try {
Collection<TopicPartition> parts = partitions.size() == 0 ? this.subscriptions.assignedPartitions() : partitions;
for (TopicPartition tp : parts) {
log.debug("Seeking to beginning of partition {}", tp);
subscriptions.needOffsetReset(tp, OffsetResetStrategy.EARLIEST);
}
} finally {
release();
}
}
查找每个给定分区的最后一个偏移量。此方法的计算比较缓慢,仅当调用@link poll(long)或时,才寻求所有分区中的最终偏移量。如果没有提供分区,则查找当前分配的所有分区的最终偏移量。如果isolation.level=read_committed
,则结束偏移量将是最后一个稳定偏移量,即具有打开事务的第一条消息的偏移量.
seekToEnd源码
public void seekToEnd(Collection<TopicPartition> partitions) {
acquire();
try {
Collection<TopicPartition> parts = partitions.size() == 0 ? this.subscriptions.assignedPartitions() : partitions;
for (TopicPartition tp : parts) {
log.debug("Seeking to end of partition {}", tp);
subscriptions.needOffsetReset(tp, OffsetResetStrategy.LATEST);
}
} finally {
release();
}
}
8、消费者拦截器
消费者拦截器:
消费者拦截器主要是在消费到消息或者提交消费位移前进行一些定制化的操作
实例:
package com.paojiaojiang.interceptor;
import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
- @Author: jja
- @Description:
- @Date: 2019/3/21 0:12
*/
public class InterceptorProducer implements Runnable{
public static String TOPIC = "paojiaopjiang";
private Producer<String, String> producer;
private ProducerConfig config = null;
public InterceptorProducer() {
Properties props = new Properties();
props.put("zookeeper.connect", "spark:2181,spark1:2181,spark2:2181");
// 指定序列化处理类,默认为kafka.serializer.DefaultEncoder,即byte[]
props.put("serializer.class", "kafka.serializer.StringEncoder");
// 同步还是异步,默认2表同步,1表异步。异步可以提高发送吞吐量,但是也可能导致丢失未发送过去的消息
props.put("producer.type", "sync");
// 是否压缩,默认0表示不压缩,1表示用gzip压缩,2表示用snappy压缩。压缩后消息中会有头来指明消息压缩类型,故在消费者端消息解压是透明的无需指定。
props.put("compression.codec", "1");
// 指定kafka节点列表,用于获取metadata(元数据),不必全部指定
props.put("metadata.broker.list", "spark:9092,spark1:9092,spark2:9092");
// 构建两个拦截器
List<String> interceptors = new ArrayList<>();
interceptors.add("com.paojiaojiang.interceptor.CountInterceptor"); // 时间拦截器
interceptors.add("com.paojiaojiang.interceptor.TimeInterceptor"); // 计数拦截器
props.put(org.apache.kafka.clients.producer.ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
config = new ProducerConfig(props);
}
@Override
public void run() {
producer = new Producer<>(config);
for (int i = 1; i <= 3; i++) { //往3个分区发数据
List<KeyedMessage<String, String>> messageList = new ArrayList<>();
for (int j = 0; j < 10; j++) { //每个分区10条消息
messageList.add(new KeyedMessage<>
//String topic, String partition, String message
(TOPIC, "partition[----" + i + "]", "message[----The " + i + "------ message]" + TOPIC));
}
System.out.println(TOPIC);
producer.send(messageList);
}
producer.close();
}
public static void main(String[] args) {
Thread t = new Thread(new com.paojiaojiang.producer.KafkaProducer1());
t.start();
}
}
时间拦截器的实现:
package com.paojiaojiang.interceptor;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
/**
- @Author: jja
- @Description: 在消息前面添加时间戳的拦截器
- @Date: 2019/3/20 23:53
*/
public class TimeInterceptor implements ProducerInterceptor<String, String> {
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
// 新建一个新的record,把时间戳写上消息的头部
String value = "paojiaojiang----->" + record.value();
return new ProducerRecord<>(record.topic(), record.partition(), record.timestamp(), record.key(), value, record.headers());
//
// return new ProducerRecord(record.topic(),
// record.partition(),
// record.timestamp(),
// record.key(),
// System.currentTimeMillis() + "," +
// record.value().toString());
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
计数拦截器的实现:
package com.paojiaojiang.interceptor;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
/**
- @Author: jja
- @Description: 统计发送的成功条数和失败条数,在producer closer前打印结果
- @Date: 2019/3/21 0:02
*/
public class CountInterceptor implements ProducerInterceptor {
private int errorCount = 0;
private int successCount = 0;
@Override
public ProducerRecord onSend(ProducerRecord record) {
return null;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
// 进行统计
if (exception == null){
successCount++;
}else {
errorCount++;
}
}
@Override
public void close() {
System.out.println("成功的条数:" + successCount);
System.out.println("失败的条数 " + errorCount);
}
@Override
public void configure(Map<String, ?> configs) {
}
}