目录
1 最简单的生产者和消费者
1.1 引入maven
1.2 基本的生产者和代码注释
1.3 最简单消费者
2 生产者发送消息的三种方式
2.1 直接send之后就不管了,会自动重试,可能丢失消息
2.2 同步非阻塞发送
2.3异步发送
3 KafkaProducer是线程安全的
3.1 多线程生产者-多线程安全
3.2 多线程消费者-多线程不安全
4 生产者常见配置
4.1 常用参数
4.2 参数详解
5 消费者常用参数
5.1 常用参数
5.2 参数详解
6 顺序保证
6.1 一个分区消息有序
6.2 保证顺序的参数配置
org.apache.kafka
kafka-clients
2.3.0
package cn.gg.hellokafka;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Properties;
/**
* MyHelloProducer
*
* @author honry.guan
* @date 2021-05-06 19:16
*/
public class MyHelloProducer {
public static void main(String[] args) {
Properties properties = new Properties();
//配置连接ip和地址
properties.put("bootstrap.servers","127.0.0.1:9092");
//kafka自带序列化器,可以不用谢全类路径StringSerializer.class也可以,这里作为演示
properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer producer = new KafkaProducer<>(properties);
try {
ProducerRecord producerRecord = new ProducerRecord<>("my-hello","name","tom");
producer.send(producerRecord);
} finally {
//关闭连接
producer.close();
}
}
}
package cn.gg.hellokafka;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
/**
* MyHelloConsumer
*
* @author honry.guan
* @date 2021-05-06 19:17
*/
public class MyHelloConsumer {
public static void main(String[] args) {
Properties properties = new Properties();
//配置连接ip和地址
properties.put("bootstrap.servers", "127.0.0.1:9092");
//kafka自带反序列化器,可以不用谢全类路径StringDeserializer.class也可以,这里作为演示
properties.put("key.deserializer", StringDeserializer.class);
properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
//要指定一个群组,否则会报错
properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test1");
KafkaConsumer kafkaConsumer = new KafkaConsumer(properties);
//订阅主题
kafkaConsumer.subscribe(Collections.singleton("my-hello"));
try {
while (true) {
//读取消息
ConsumerRecords consumerRecords = kafkaConsumer.poll(Duration.ofMillis(500));
for (ConsumerRecord record : consumerRecords) {
System.out.println(String.format("topic:%s,分区:%d,偏移量:%d," + "key:%s,value:%s", record.topic(), record.partition(),
record.offset(), record.key(), record.value()));
}
}
} finally {
kafkaConsumer.close();
}
}
}
ProducerRecord producerRecord = new ProducerRecord<>("my-hello","name","tom");
//发送失败会重新发送,但是可能会丢失数据
producer.send(producerRecord);
获得send方法返回的Future对象,在合适的时候调用Future的get方法
Future future = producer.send(record);
System.out.println("发送数据之后,先做其他操作");
//阻塞在这个位置
RecordMetadata recordMetadata = future.get();
if(null!=recordMetadata){
System.out.println("offset:"+recordMetadata.offset()+"-" +"partition:"+recordMetadata.partition());
}
实现接口org.apache.kafka.clients.producer.Callback,然后将实现类的实例作为参数传递给send方法
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata,
Exception exception) {
if(null!=exception){
exception.printStackTrace();
}
if(null!=metadata){
System.out.println("offset:"+metadata.offset()+"-"
+"partition:"+metadata.partition());
}
}
});
可以使用同一个生产者发送消息,多个线程使用的同一个生产者
package cn.gg.concurrent;
import cn.enjoyedu.config.KafkaConst;
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author: honry.guan
* @create: 2021-05-06 21:05
**/
public class MyConProducer extends Thread {
//自定义线程池
static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
static CountDownLatch countDownLatch = new CountDownLatch(1000);
static KafkaProducer producer = new KafkaProducer(KafkaConst.producerConfig(StringSerializer.class, StringSerializer.class));
ProducerRecord record;
public MyConProducer(ProducerRecord record) {
this.record = record;
}
@Override
public void run() {
final String id = Thread.currentThread().getId()
+ "-" + System.identityHashCode(producer);
try {
//异步发送,
producer.send(record, new Callback() {
public void onCompletion(RecordMetadata metadata,
Exception exception) {
if (null != exception) {
exception.printStackTrace();
}
if (null != metadata) {
System.out.println(id + "|" + String.format("偏移量:%s,分区:%s",metadata.offset(), metadata.partition()));
}
}
});
countDownLatch.countDown();
System.out.println(id + ":数据[" + record.toString() + "]已发送。");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
for (int i = 0; i < 1000; i++) {
ProducerRecord record = new ProducerRecord("concurrent-test", null, i + "", i + "");
executorService.submit(new MyConProducer(record));
}
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
} finally {
producer.close();
executorService.shutdown();
}
}
}
import cn.enjoyedu.config.BusiConst;
import cn.enjoyedu.config.KafkaConst;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author: honry.guan
* @create: 2021-05-06 21:22
**/
public class MyConConsumer extends Thread {
private static ExecutorService executorService = Executors.newFixedThreadPool(BusiConst.CONCURRENT_PARTITIONS_COUNT);
/*消费配置的实例*/
static Map config
= KafkaConst.consumerConfigMap("concurrent",
StringDeserializer.class,
StringDeserializer.class);
KafkaConsumer consumer;
public MyConConsumer(KafkaConsumer consumer) {
this.consumer = consumer;
//订阅主题
this.consumer.subscribe(Collections.singletonList("concurrent-test"));
}
public void run() {
final String id = Thread.currentThread().getId() + "-" + System.identityHashCode(consumer);
try {
while (true) {
ConsumerRecords records = consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord record : records) {
System.out.println(id + "|" + String.format("主题:%s,分区:%d,偏移量:%d," + "key:%s,value:%s",
record.topic(), record.partition(),
record.offset(), record.key(), record.value()));
//做其他的工作
}
}
} finally {
consumer.close();
}
}
public static void main(String[] args) {
//每个线程都有属于自己的消费者
for (int i = 0; i < BusiConst.CONCURRENT_PARTITIONS_COUNT; i++) {
executorService.submit(new MyConConsumer(new KafkaConsumer(config)));
}
}
}
public class ConfigKafkaProducer {
public static void main(String[] args) {
//生产者三个属性必须指定(broker地址清单、key和value的序列化器)
Properties properties = new Properties();
properties.put("bootstrap.servers", "127.0.0.1:9092");
properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//更多发送配置(重要的)
// ack 0,1,all
properties.put("acks", "1");
// 一个批次可以使用的内存大小 缺省16384(16k)
properties.put("batch.size", 16384);
// 指定了生产者在发送批次前等待更多消息加入批次的时间, 缺省0 50ms
properties.put("linger.ms", 0L);
// 控制生产者发送请求最大大小,默认1M (这个参数和Kafka主机的message.max.bytes 参数有关系)
properties.put("max.request.size", 1 * 1024 * 1024);
//非重要的
// 生产者内存缓冲区大小
properties.put("buffer.memory", 32 * 1024 * 1024L);
//重发消息次数
properties.put("retries", 0);
//客户端将等待请求的响应的最大时间 默认30秒
properties.put("request.timeout.ms", 30 * 1000);
//最大阻塞时间,超过则抛出异常 缺省60000ms
properties.put("max.block.ms", 60 * 1000);
// 于压缩数据的压缩类型。默认是无压缩 ,none、gzip、snappy
properties.put("compression.type", "none");
}
}
acks:指定了必须要有多少个分区副本收到消息,对消息丢失有重大影响 |
金融业务,主备外加异地灾备。所以很多高可用场景一般不是设置 2 个副本,有可能达到 5 个副本,不同机架上部署不同的副本,异地上也部署一 套副本。 buffer.memory 设置生产者内存缓冲区的大小(结合 生产者发送消息的基本流程 ),生产者用它缓冲要发送到服务器的消息。如果数据产生速度大于向 broker 发送 的速度,导致生产者空间不足, producer 会阻塞或者抛出异常。缺省 33554432 (32M)
|
max.block.ms:指定了在调用 send()方法或者使用 partitionsFor()方法获取元数据时生产者的阻塞时间。 |
当生产者的发送缓冲区已满,或者没有可用的元数据时,这些 方法就会阻塞。在阻塞时间达到 max.block.ms 时,生产者会抛出超时异常。缺省 60000ms
|
retries:发送失败时,指定生产者可以重发消息的次数(缺省 Integer.MAX_VALUE) |
默认情况下,生产者在每次重试之间等待 100ms,可以通过参数 retry.backoff.ms 参数来改变这个时间间隔。
|
receive.buffer.bytes 和send.buffer.bytes:指定 TCP socket 接受和发送数据包的缓存区大小。 |
如果它们被设置为 -1,则使用操作系统的默认值。如果生产者或消费者处在不同的数据中心,那么 可以适当增大这些值,因为跨数据中心的网络一般都有比较高的延迟和比较低的带宽。缺省 102400
|
batch.size: 当多个消息被发送同一个分区时,生产者会把它们放在同一个批次里。
|
该参数指定了一个批次可以使用的内存大小,按照字节数计算。当批次内存 被填满后,批次里的所有消息会被发送出去。但是生产者不一定都会等到批次被填满才发送,半满甚至只包含一个消息的批次也有可能被发送。缺省 16384(16k) ,如果一条消息超过了批次的大小,会写不进去。
|
linger.ms: 指定了生产者在发送批次前等待更多消息加入批次的时间。
|
它和 batch.size 以先到者为先。也就是说,一旦我们获得消息的数量够 batch.size 的数量 了,他将会立即发送而不顾这项设置,然而如果我们获得消息字节数比 batch.size 设置要小的多,我们需要 “linger” 特定的时间以获取更多的消息。这个设
置默认为 0 ,即没有延迟。设定 linger.ms=5 ,例如,将会减少请求数目,但是同时会增加 5ms 的延迟,但也会提升消息的吞吐量。 compression.type producer 用于压缩数据的压缩类型。默认是无压缩。正确的选项值是 none 、 gzip 、 snappy 。压缩最好用于批量处理,批量处理消息越多,压缩性能越
好。 snappy 占用 cpu 少,提供较好的性能和可观的压缩比,如果比较关注性能和网络带宽,用这个。如果带宽紧张,用 gzip ,会占用较多的 cpu ,但提供
更高的压缩比。
|
client.id:当向 server 发出请求时,这个字符串会发送给 server。 |
目的是能够追踪请求源头,以此来允许 ip/port 许可列表之外的一些应用可以发送信息。这项 应用可以设置任意字符串,因为没有任何功能性的目的,除了记录和跟踪。
|
max.in.flight.requests.per.connection |
指定了生产者在接收到服务器响应之前可以发送多个消息,值越高,占用的内存越大,当然也可以提升吞吐量。发生错误时,可能会造成数据的发 送顺序改变 , 默认是 5 ( 修改)。
如果需要保证消息在一个分区上的严格顺序,这个值应该设为 1 。不过这样会严重影响生产者的吞吐量。
|
request.timeout.ms |
客户端将等待请求的响应的最大时间 , 如果在这个时间内没有收到响应,客户端将重发请求 ; 超过重试次数将抛异常,默认 30 秒。
|
metadata.fetch.timeout.ms |
是指我们所获取的一些元数据的第一个时间数据。元数据包含: topic , host , partitions 。此项配置是指当等待元数据 fetch 成功完成所需要的时间, 否则会跑出异常给客户端
|
max.request.size |
控制生产者发送请求最大大小。默认这个值为 1M ,如果一个请求里只有一个消息,那这个消息不能大于 1M,如果一次请求是一个批次,该批次包 含了 1000 条消息,那么每个消息不能大于 1KB 。注意: broker 具有自己对消息记录尺寸的覆盖,如果这个尺寸小于生产者的这个设置,会导致消息被拒
绝。这个参数和 Kafka 主机的 message.max.bytes 参数有关系。如果生产者发送的消息超过 message.max.bytes 设置的大小,就会被 Kafka 服务器拒绝。
|
public class ConfigKafkaConsumer {
public static void main(String[] args) {
//消费者三个属性必须指定(broker地址清单、key和value的反序列化器)
Properties properties = new Properties();
properties.put("bootstrap.servers","127.0.0.1:9092");
properties.put("key.deserializer", StringDeserializer.class);
properties.put("value.deserializer", StringDeserializer.class);
//群组
properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test1");
//更多消费者配置(重要的)
properties.put("auto.offset.reset","latest"); //消费者在读取一个没有偏移量的分区或者偏移量无效的情况下,如何处理
properties.put("enable.auto.commit",true); // 表明消费者是否自动提交偏移 默认值true
properties.put("max.poll.records",500); // 控制每次poll方法返回的的记录数量 默认值500
//分区分配给消费者的策略。系统提供两种策略。默认为Range
properties.put("partition.assignment.strategy",Collections.singletonList(RangeAssignor.class));
KafkaConsumer consumer = new KafkaConsumer(properties);
}
}
auto.offset.reset |
消费者在读取一个没有偏移量的分区或者偏移量无效的情况下,如何处理。
|
enable .auto.commit | 默认值 true,表明消费者是否自动提交偏移。为了尽量避免重复数据和数据丢失,可以改为 false,自行控制何时提交。 |
partition.assignment.strategy |
分区分配给消费者的策略。系统提供两种策略。默认为 Range 。允许自定义策略。
extends 类 AbstractPartitionAssignor ,然后在消费者端增加参数:
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, 类 .class.getName()); 即可。
|
max.poll.records | 控制每次 poll 方法返回的的记录数量。 |
fetch.min.bytes |
每次 fetch 请求时, server 应该返回的最小字节数。如果没有足够的数据返回,请求会等待,直到足够的数据才会返回。缺省为 1 个字节。多消费者 下,可以设大这个值,以降低 broker 的工作负载
|
fetch.wait.max.ms |
如果没有足够的数据能够满足 fetch.min.bytes ,则此项配置是指在应答 fetch 请求之前, server 会阻塞的最大时间。缺省为 500 个毫秒。和上面的 fetch.min.bytes 结合起来,要么满足数据的大小,要么满足时间,就看哪个条件先满足。
|
max.partition.fetch.bytes |
指定了服务器从每个分区里返回给消费者的最大字节数,默认 1MB 。假设一个主题有 20 个分区和 5 个消费者,那么每个消费者至少要有 4MB 的可
用内存来接收记录,而且一旦有消费者崩溃,这个内存还需更大。注意,这个参数要比服务器的 message.max.bytes 更大,否则消费者可能无法读取消息。
|
session.timeout.ms
|
如果 consumer 在这段时间内没有发送心跳信息,则它会被认为挂掉了。默认 3 秒。 |
client.id
|
当向 server 发出请求时,这个字符串会发送给 server 。目的是能够追踪请求源头,以此来允许 ip/port 许可列表之外的一些应用可以发送信息。这项
应用可以设置任意字符串,因为没有任何功能性的目的,除了记录和跟踪。
|
receive.buffer.bytes 和 send.buffer.bytes |
指定 TCP socket 接受和发送数据包的缓存区大小。如果它们被设置为 -1 ,则使用操作系统的默认值。如果生产者或消费者处在不同的数据中心,那么
可以适当增大这些值,因为跨数据中心的网络一般都有比较高的延迟和比较低的带宽。
|