kafka02-Java客户端
kafka02-Java客户端
1.Kafka客户端-Java开发环境搭建
- 导入依赖
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.12.0</version>
</dependency>
</dependencies>
- 日志配置文件。
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="error" strict="true" name="XMLConfig">
<Appenders>
<!-- 类型名为Console,名称为必须属性 -->
<Appender type="Console" name="STDOUT">
<!-- 布局为PatternLayout的方式,
输出样式为[INFO] [2018-01-22 17:34:01][org.test.Console]I'm here -->
<Layout type="PatternLayout"
pattern="[%p] [%d{yyyy-MM-dd HH:mm:ss}][%c{10}]%m%n" />
</Appender>
</Appenders>
<Loggers>
<!-- 可加性为false -->
<Logger name="test" level="info" additivity="false">
<AppenderRef ref="STDOUT" />
</Logger>
<!-- root loggerConfig设置 -->
<Root level="info">
<AppenderRef ref="STDOUT" />
</Root>
</Loggers>
</Configuration>
2.Producer API
- 异步发送不带回调。
public static void producerMessage() {
Properties props = new Properties();
//kafka集群,对应生产者客户端命令 broker-list
props.put("bootstrap.servers", "192.168.253.134:9092");
props.put("acks", "all");
//重试次数
props.put("retries", 3);
//批次大小
props.put("batch.size", 16384);
//等待时间
props.put("linger.ms", 1);
//RecordAccumulator缓冲区大小
props.put("buffer.memory", 33554432);
// key和value的序列化方式
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);
for (int i = 0;i < 10;i++) {
ProducerRecord<String, String> record = new ProducerRecord<>("offsetdemo01", "message" + i);
kafkaProducer.send(record);
}
kafkaProducer.close();
}
- 异步发送带回调。
public static void producerMessageCallback() {
Properties props = new Properties();
...
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);
for (int i = 0;i < 10;i++) {
ProducerRecord<String, String> record = new ProducerRecord<>("offsetdemo01", "message####" + i);
kafkaProducer.send(record, new Callback() {
/**
* 消费发送成功exception为null,失败exception不为null。
* @param metadata
* @param exception
*/
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception != null) {
System.out.println("消息发送失败!!!");
}else {
System.out.println("消费发送成功," + metadata.topic() + " -- " +
metadata.partition() + " -- " + metadata.offset());
}
}
});
}
kafkaProducer.close();
}
- 同步发送。
public static void producerMessageFor() {
Properties props = new Properties();
...
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);
for (int i = 0;i < 10;i++) {
ProducerRecord<String, String> record = new ProducerRecord<>("offsetdemo01", "message####" + i);
Future<RecordMetadata> send = kafkaProducer.send(record, new Callback() {
/**
* 消费发送成功exception为null,失败exception不为null。
* @param metadata
* @param exception
*/
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception != null) {
System.out.println("消息发送失败!!!");
} else {
System.out.println("消费发送成功," + metadata.topic() + " -- " +
metadata.partition() + " -- " + metadata.offset());
}
}
});
System.out.println("==== 消息发送 ====");
try {
// 使用Future.get()来阻塞发送消息的线程,指定消息发送成功并收到ACK后。
send.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println("00000 消息发送成功 00000");
}
kafkaProducer.close();
}
4.Kafka自定义分区器
public class MyPartition implements Partitioner {
/**
*
* @param topic 主题
* @param key key
* @param keyBytes key序列化后的字节数组
* @param value 消息
* @param valueBytes 消息序列化后的字节数组
* @param cluster 集群对象
* @return
*/
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
if (value instanceof String) {
String s = (String) value;
if (s.contains("1")) {
return 0;
} else {
return 1;
}
}
return 0;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
// 使用自定义分区器发送消息
public static void producerMessageCustomPartition() {
Properties props = new Properties();
...
// 指定使用定义的分区器对象
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, "com.my.kafka.demo.MyPartition");
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);
for (int i = 0;i < 10;i++) {
ProducerRecord<String, String> record = null;
if (i == 1 || i == 5) {
record = new ProducerRecord<>("offsetdemo01", "message####" + i);
} else {
record = new ProducerRecord<>("offsetdemo01", UUID.randomUUID().toString());
}
kafkaProducer.send(record, new Callback() {
/**
* 消费发送成功exception为null,失败exception不为null。
* @param metadata
* @param exception
*/
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception != null) {
System.out.println("消息发送失败!!!");
} else {
System.out.println("消费发送成功," + metadata.topic() + " -- " +
metadata.partition() + " -- " + metadata.offset());
}
}
});
}
kafkaProducer.close();
}
3.Consumer API
-
Consumer消费数据时的可靠性是很容易保证的,因为数据在Kafka中是持久化的,故不用担心数据丢失问题。但是Consumer在消费过程中可能会出现断电宕机等故障,Consumer恢复后,需要从故障前的位置的继续消费,所以Consumer需要实时记录自己消费到了哪个offset,以便故障恢复后继续消费。因此offset的维护是Consumer消费数据是必须考虑的问题。
-
自动提交offset。
public static void consumerMessage() {
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.253.134:9092");
// 消费者组
props.put("group.id", "consumer-test");
// 自动提交offset
props.put("enable.auto.commit", "true");
// 自动提交的间隔时间
props.put("auto.commit.interval.ms", "1000");
// key和value的反序列化
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<>(props);
List<String> partitioners = new ArrayList<>();
partitioners.add("offsetdemo01");
// 可以订阅没有的topic,订阅之后kafka会使用默认值创建该topic,一个分区,一个副本。
//partitioners.add("hello01");
kafkaConsumer.subscribe(partitioners);
while (true) {
// 每隔5秒钟拉取一次消息
ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(5));
for (ConsumerRecord<String, String> record : records) {
System.out.println("消费消息==" + record.topic() + " --" + record.partition() + " -- "
+ record.key() + " -- " + record.value());
}
}
}
- offset重置。
/**
* Offset重置。
* 当kafka找不到offset时会进行offset的重置。
* kafka重置offset的两种情况。
* 1 新加入的消费者组,没有消费记录,没有offset,会进行重置。
* 2 offset,1-20,距离当前时间超过7天,kafka需要消费1-20的offset,这是找不到
* 这些消息,就会进行offset的重置。消费7天前的已经被删除的消息。
*
* 设置offset的重置策略。auto.offset.reset
* earliest: automatically reset the offset to the earliest offset 最早
* latest: automatically reset the offset to the latest offset 最晚
* none 没有offset就会抛出异常
*/
public static void consumerMessageOffset() {
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.253.134:9092");
// 消费者组
props.put("group.id", "consumer-test-offset");
// 是否自动提交
props.put("enable.auto.commit", "true");
// 自动提交的间隔时间
props.put("auto.commit.interval.ms", "1000");
// 设置offset重置策略,设置为earliest,就可以消费到历史的消息。
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
// key和value的反序列化
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<>(props);
List<String> partitioners = new ArrayList<>();
partitioners.add("offsetdemo01");
// 可以订阅没有的topic,订阅之后kafka会使用默认值创建该topic,一个分区,一个副本。
//partitioners.add("hello01");
kafkaConsumer.subscribe(partitioners);
while (true) {
// 设置超时时间
ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(2));
for (ConsumerRecord<String, String> record : records) {
System.out.println("消费消息==" + record.topic() + " --" + record.partition() + " -- "
+ record.key() + " -- " + record.value());
}
}
}
-
手动提交offset。
- 手动提交offset的方法有两种:commitSync同步提交和commitAsync异步提交。
- 两者的相同点是:都会将本次poll的一批数据最高的偏移量提交。
- 两者的不同点是:commitSync阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而commitAsync则没有失败重试机制,故有可能提交失败。
- 同步和异步提交offset。
//异步提交Offset ,== {offsetdemo01-1=OffsetAndMetadata{offset=588, leaderEpoch=0, metadata=''}, offsetdemo01-0=OffsetAndMetadata{offset=987, leaderEpoch=0, metadata=''}} /** * 手动提交offset。 * enable.auto.commit=false。 * 消费者组第一次启动后,都会获取offset,然后进行消息的消费。这时候每次消费完成, * 如果没有进行消费的提交,会将offset维护在内存中。 * * offset手动提交。 * 同步提交,提交offset后再消费下一条消息。 * 异步提交,提交offset时就可以消费下一条消息。 */ public static void consumerMessageCommitOffset() { Properties props = new Properties(); props.put("bootstrap.servers", "192.168.253.134:9092"); // 消费者组 props.put("group.id", "consumer-test-offset"); // 是否自动提交 props.put("enable.auto.commit", "false"); // key和value的反序列化 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<>(props); List<String> partitioners = new ArrayList<>(); partitioners.add("offsetdemo01"); kafkaConsumer.subscribe(partitioners); while (true) { // 拉取消息的间隔时间 ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(2)); for (ConsumerRecord<String, String> record : records) { System.out.println("消费消息==" + record.topic() + " --" + record.partition() + " -- " + record.key() + " -- " + record.value()); } System.out.println("00 开始提交offset 00"); // 同步提交Offset,会阻塞当前线程,直到提交完成。 //kafkaConsumer.commitSync(); // 异步提交Offset kafkaConsumer.commitAsync(new OffsetCommitCallback() { @Override public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) { System.out.println("异步提交Offset ,== " + offsets); } }); System.out.println("AA Offset提交完成 AA"); } }
-
数据漏消费和重复消费。无论是同步提交还是异步提交offset,都有可能会造成数据的漏消费或者重复消费。先提交offset后消费,有可能造成数据的漏消费;而先消费后提交offset,有可能会造成数据的重复消费。
5.Kafka自定义Interceptor拦截器
- Interceptor拦截器使得用户在消息发送前以及producer回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。
- 自定义拦截器。
/**
* 自定义拦截器,在消息前加时间戳
*/
public class TimeInterceptor implements ProducerInterceptor<String, String> {
/**
* 消息前加时间戳。
* 该方法封装进KafkaProducer.send方法中,即它运行在用户主线程中。
* Producer确保在消息被序列化以及计算分区前调用该方法。
* 用户可以在该方法中对消息做任何操作,但最好保证不要修改消息所属的topic和分区,
* 否则会影响目标分区的计算。
*/
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
String value = record.value();
String timeValue = System.currentTimeMillis() + " ->> " + value;
ProducerRecord<String, String> newRecord = new ProducerRecord<>(record.topic(), record.partition(),
record.key(), timeValue);
return newRecord;
}
/**
* 该方法会在消息从RecordAccumulator成功发送到Kafka Broker之后,
* 或者在发送过程中失败时调用。并且通常都是在producer回调逻辑触发之前。
* onAcknowledgement运行在producer的IO线程中,
* 因此不要在该方法中放入很重的逻辑,否则会拖慢producer的消息发送效率。
*/
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
/**
* 关闭interceptor,主要用于执行一些资源清理工作
*/
@Override
public void close() {
}
/**
* 获取配置信息和初始化数据时调用。
* @param configs
*/
@Override
public void configure(Map<String, ?> configs) {
}
}
/**
* 定义拦截器,统计生产成功和失败的个数
*/
public class CountInterceptor implements ProducerInterceptor<String, String> {
private int success;
private int fail;
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
return record;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
if (exception != null) {
fail++;
} else {
success++;
}
}
@Override
public void close() {
System.out.println("消息生产成功 -> " + success);
System.out.println("消息生产失败 -> " + fail);
}
@Override
public void configure(Map<String, ?> configs) {
}
}
- 生产者使用拦截器。
public class ProducerInterceptor {
public static void main(String[] args) {
Properties props = new Properties();
//kafka集群,对应生产者客户端命令 broker-list
props.put("bootstrap.servers", "192.168.253.134:9092");
props.put("acks", "all");
//重试次数
props.put("retries", 3);
//批次大小
props.put("batch.size", 16384);
//等待时间
props.put("linger.ms", 1);
//RecordAccumulator缓冲区大小
props.put("buffer.memory", 33554432);
// key和value的队列化方式
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 设置拦截器,拦截器的调用顺序就是添加到List的顺序
List<String> interceptors = new ArrayList<>();
interceptors.add("com.my.kafka.demo.incerptor.TimeInterceptor");
interceptors.add("com.my.kafka.demo.incerptor.CountInterceptor");
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);
for (int i = 0;i < 10;i++) {
ProducerRecord<String, String> record = new ProducerRecord<>("offsetdemo01", "message" + i);
kafkaProducer.send(record);
}
kafkaProducer.close();
}
}