kafka 版本 2.1.0
1 Producer API
1.1 原生
引入maven文件
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.1.0</version>
</dependency>
KafkaProducer<K,V> 是Kafka集群发送消息的客户端
KafkaProducer是线程安全的,多个线程共享一个KafkaProducer实例将会比多个线程更快
下面是一个示例
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<>(props);
for (int i = 0; i < 100; i++)
producer.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), Integer.toString(i)));
producer.close();
KafkaProducer是由一个Buffer空间和一个I/O线程组成。其中,Buffer保存尚未传输到服务器的消息,I/O线程负责将这些消息转换为请求并将他们传输到集群。使用后,如果关闭资源失败会导致资源泄漏
send() 方法是一步的。调用时,它将record添加到buffer中国呢并立即返回。这允许producer批量高效发送record
ack配置控制用于请求完成的条件。示例中指定了All
如果请求失败了,producer可以自动重试,除非我们设置重试次数为0。启用重试也会导致重复消费的问题
Producer为每个分区未发送的record维护了不同的buffer。buffer大小由batch.size配置。设置增大可以导致更多的批处理,但是需要更多的内存
默认情况下,即使未发送的的record未达到batch.size大小,我们也会发送。由配置linger.ms设置。这表示producer在发送请求之前等待的毫秒数,以希望同一批次可以发送更多的record
buffer.memory控制producer可用于缓冲的内存总量。如果记录的发送速度超过了将记录发送到服务器的速度,则该缓冲区空间将被耗尽。当缓冲区空间用尽时,其他发送调用将阻塞。阻止时间由max.block.ms配置
key.serializer和value.serializer 是ProducerRecord键值序列化的方法
生产者分为幂等生产者和事务生产者
要启用幂等,必须将enable.idempotence配置设置为true。如果设置,则重试配置将默认为Integer.MAX_VALUE,而acks配置将默认为全部。幂等生产者没有API更改,因此无需修改现有应用程序即可利用此功能。
要利用幂等生成器,必须避免重新发送应用程序级别,因为这些应用程序无法重复删除。因此,如果应用程序启用幂等性,建议将重试配置保留为未设置状态,因为它将默认为Integer.MAX_VALUE。此外,如果send(ProducerRecord)即使无限次重试也返回错误(例如,如果消息在发送之前已在缓冲区中过期),则建议关闭生产者并检查最后产生的消息的内容,以确保不能重复。最后,生产者只能保证在单个会话中发送的消息具有幂等性。要使用事务产生器和附带的API,必须设置transactional.id配置属性。如果设置了transactional.id,则会自动启用幂等性以及幂等性所依赖的生产者配置。此外,交易中包含的主题应配置为具有持久性。特别是,replication.factor应该至少为3,并且这些主题的min.insync.replicas应该设置为2。最后,为了从端到端实现事务保证,必须使使用者配置为也仅读取已提交的消息。transactional.id的目的是启用单个生产者实例的多个会话之间的事务恢复。它通常从分区的有状态应用程序中的分片标识符派生。因此,它对于在分区应用程序中运行的每个生产者实例应该是唯一的。所有新的事务性API都将被阻止,并且将在失败时引发异常。下面的示例说明了如何使用新的API。除了所有100条消息都是单个事务的一部分之外,它与上面的示例相似。
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("transactional.id", "my-transactional-id");
Producer<String, String> producer = new KafkaProducer<>(props, new StringSerializer(), new StringSerializer());
producer.initTransactions();
try {
producer.beginTransaction();
for (int i = 0; i < 100; i++)
producer.send(new ProducerRecord<>("my-topic", Integer.toString(i), Integer.toString(i)));
producer.commitTransaction();
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
// We can't recover from these exceptions, so our only option is to close the producer and exit.
producer.close();
} catch (KafkaException e) {
// For all other exceptions, just abort the transaction and try again.
producer.abortTransaction();
}
producer.close();
2 Consumer API
引入Maven
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.1.0</version>
</dependency>
KafkaConsumer 处理Kafka broker节点的估值,应对集群内topic分区的调整 。使用consumer group处理消费负责。
consumer维持一个和broker节点的tcp连接。consumer不是线程安全的。
Offset:偏移量
消费者Group和订阅
Consumer Group 提供了消费者的伸缩性和容错性。
Group中成员通过分区策略消费某几个分区。Topic中一个分区只能分配给一个消费者
检测消费者存活Alive
订阅Topiic 后,消费者将会加入group poll数据。如果consumer在session.timeout.ms期间崩溃或无法发送heatbeaters,则该consumer 被视为已死,其负责的分区将被重新分配
示例
自动提交
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("hetao_test"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records){
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
}
bootstrap.servers: 指定kafka节点代理获取连接
enable.auto.commit: 自动提交 配合auto.commit.interval.ms. 使用
手动提交
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "false");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(props);
kafkaConsumer.subscribe(Arrays.asList("hetao_test"));
final int minBatchSize = 200;
List<ConsumerRecord<String, String>> buffer = new ArrayList<>();
while (true){
ConsumerRecords<String, String> records = kafkaConsumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
buffer.add(record);
}
if(buffer.size() >= minBatchSize){
insertIntoDb(buffer);
kafkaConsumer.commitSync();
buffer.clear();
}
}
在示例中,我们将一批记录批处理放到内存中,到一定量时,批量插入到数据库。
如果是自动提交的话,在将记录poll后,就已经提交,但是此时插入数据库有可能失败
为了避免这种情况,我们仅在将相应的记录插入数据库后才手动提交offset,这样可以精确控制消费的offset
但是,在插入数据库之后,但在提交之前的间隔内也可能失败(几毫米),在这种情况下,需要重复消费。
上面的示例使用commitSync将所有接收到的记录标记为已提交。在某些情况下,您可能希望通过显示指定偏移量来更好地控制已提交的记录。
try {
while(running) {
ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
for (TopicPartition partition : records.partitions()) {
List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
for (ConsumerRecord<String, String> record : partitionRecords) {
System.out.println(record.offset() + ": " + record.value());
}
long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));
}
}
} finally {
consumer.close();
}
注意:提交的offset应该始终是下一条消息的offset。因此,在调用commitSync(offsets)时,应在处理的消息的偏移量上添加应该
手动设定订阅的分区
String topic = "foo";
TopicPartition partition0 = new TopicPartition(topic, 0);
TopicPartition partition1 = new TopicPartition(topic, 1);
consumer.assign(Arrays.asList(partition0, partition1));
外置存储offset
控制消费者的position
Kafka允许使用seek(TopicPartition,long)指定位置以指定新位置。还可以使用特殊方法来查找服务器维护的最早和最新偏移(分别为seekToBeginning(Collection)和seekToEnd(Collection))。
消费流量控制
Kafka通过使用pause(Collection)和resume(Collection)在指定的分配分区上暂停消耗并在未来的poll(Duration)调用中分别在指定的已暂停分区上恢复消耗,来支持消耗流的动态控制。
Read 原子事务消息
事务是在Kafka 0.11.0中引入的,其中应用程序可以原子地写入多个主题和分区。为了使它起作用,应该将从这些分区读取的使用者配置为仅读取已提交的数据。这可以通过在使用者的配置中设置Isolation.level = read_committed来实现。
多线程
Kafka Consumer不是线程安全。所有网络I / O都发生在进行调用的应用程序线程中。需要自己保证同步。否则会抛出异常ConcurrentModificationException
该规则的唯一例外是wakeup(),可以从外部线程安全地使用它来中断活动操作。在这种情况下,将从该操作的线程阻塞中抛出WakeupException。这可用于从另一个线程关闭使用者。以下代码段显示了典型模式:
public class KafkaConsumerRunner implements Runnable {
private final AtomicBoolean closed = new AtomicBoolean(false);
private final KafkaConsumer consumer;
public void run() {
try {
consumer.subscribe(Arrays.asList("topic"));
while (!closed.get()) {
ConsumerRecords records = consumer.poll(Duration.ofMillis(10000));
// Handle new records
}
} catch (WakeupException e) {
// Ignore exception if closing
if (!closed.get()) throw e;
} finally {
consumer.close();
}
}
// Shutdown hook which can be called from a separate thread
public void shutdown() {
closed.set(true);
consumer.wakeup();
}
}
然后,在单独的线程中,可以通过设置已关闭标志并唤醒使用者来关闭使用者。
closed.set(true);
consumer.wakeup();