正常的发送逻辑
- 配置生产者客户端参数及创建相对应的生产者实例。
- 构建待发送消息。
- 发送消息。
- 关闭生产者实例。
生产者代码如下:
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.3.1</version>
</dependency>
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class ProducerStart {
private static final String topic = "topic-demo";
private static final String brokeList = "localhost:9092";
public static Properties initConfig() {
Properties props = new Properties();
props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokeList);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.CLIENT_ID_CONFIG, "producer.client.id.demo");
// props.put("acks", "all");
// props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// props.put("client.id", "producer.client.id.demo");
// props.put("bootstrap.server", brokeList);
return props;
}
public static void main(String[] args) {
Properties properties = initConfig();
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
ProducerRecord<String, String> record = new ProducerRecord<>(topic, "hello,Kafka!");
try {
producer.send(record);
} catch (Exception e) {
e.printStackTrace();
}
producer.close();
}
}
先来看看KafkaProducer类的注释,一般来说,开源项目源码都是言简意赅
The producer consists of a pool of buffer space that holds records that haven't yet been transmitted to the server
as well as a background I/O thread that is responsible for turning these records into requests and transmitting them to the cluster. Failure to close the producer after use will leak these resources.
The {@link #send(ProducerRecord) send()} method is asynchronous. When called it adds the record to a buffer of pending record sends
and immediately returns. This allows the producer to batch together individual records for efficiency.
The <code>acks</code> config controls the criteria under which requests are considered complete. The "all" setting
we have specified will result in blocking on the full commit of the record, the slowest but most durable setting.
我来翻译一下:
第一句意思是生成者由一个缓冲空间池组成,其中保存了尚未传输到服务器的消息
以及有个后台I/O线程负责将这些消息转换为请求并将其传输到集群的。这说明Producer类并不是真正的发送类,而是将消息存储起来,等待另一个线程将消息发送到服务端。
第二句,send()方法是异步的,当被调用时,它将消息添加到挂起消息发送的缓冲区中并立即返回。这允许生产者批量处理单个记录以提高效率。这里也说明Producer不是真正的发送者,而是将消息存在缓冲区中就返回。
第三局,acks配置控制了被认为是完整请求的条件。示例代码中指定acks=all,这将导致阻塞记录的完整提交,这是最慢但最持久的设置。查看org.apache.kafka.clients.producer.ProducerConfig#ACKS_CONFIG发现,下面还有个ACKS的解释ACKS_DOC。
这个参数用来指定分区中必须有多少个副本收到这条消息,之后生产者才会认为这条消息是成功写入的。acks有3种类型的值(字符串类型)
- acks = 0 。那么生产者将根本不会等待任何来自服务器的确认。该记录将立即被添加到套接字缓冲区并被认为已发送。在这种情况下,不能保证服务器已经收到了记录,重试配置不会生效(因为客户端通常不会知道任何失败)。返回给每条记录的偏移量将会总是设置为-1。
- acks = 1。这将意味着leader将记录写到它的本地日志中,就会响应成功,不需要等待其他副本的确认。在这种情况下,如果leader在确认记录之后失败了,但是在follow副本复制之前,记录就会丢失。
- acks = -1 或 acks = all。这意味着leader将等待完整的同步副本集合(in-sync replicas)承认记录。这保证了只要有至少一个同步副本,记录就不会丢失.acks=all可以达到最强的可靠性
消息的发送
发送消息主要有三种模式:发后即忘(fire-and-forget),同步(sync),异步(async)。示例代码中用的就是发后即忘,只管往Kafka中发送消息而不关系消息是否正确到达。在大多数情况下,这种发送方式没有什么问题,不过在某些时候会造成消息的丢失。该方式性能最高,可靠性最差。
KakfaProducer的send()方法是 Future 类型。send有两个重载方法,具体定义如下:
public Future<RecordMetadata> send(ProducerRecord<K, V> record) {
return send(record, null);
}
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback)
同步,可以利用返回的Future对象实现,示例如下:
try {
producer.send(record).get();
} catch (Exception e) {
e.printStackTrace();
}
实际上send方法本身就是异步的。调用get()方法来阻塞等待kafka的响应,直到消息发送成功或者发生异常。如果发生异常,那么需要捕获并交由外层逻辑处理。
KakfaProducer中一般会出现两种类型异常:一种是可重试异常与不可重试异常。
对于可重试异常,如果配置了retries参数,那么只要在规定的重试次数内自行恢复,就不会抛出异常。retries参数默认为0,配置方式如下:
props.put(ProducerConfig.RETRIES_CONFIG,3);
//这表示配置了3次重试,如果重试了3次之后还没有恢复,那么仍会抛出异常,进外层逻辑就要处理这些异常了
同步发送的方式性能很低,需要阻塞等待一条消息发送完以后才能发出下一条。
异步发送的方式
一般是send()方法里面指定一个Callback的回调函数,Kafka在返回响应时调用该函数来实现异步的发送确认。 使用Callback的方式非常简单明了,Kafka有响应就会回调,要么发送成功要么抛出异常,发送方式如下:
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception != null) {
//异常不为空,说明发送失败
//实际项目中可以根据具体业务进行相应的处理
exception.printStackTrace();
} else {
//说明消息发送成功
System.out.println(metadata.topic() + "-" + metadata.partition() + ":" + metadata.offset());
}
}
});
onCompletion()方法里面的两个参数是互斥的。之所以是互斥的,是因为这个。这个下次再讲
org.apache.kafka.clients.producer.internals.ProducerBatch#completeFutureAndFireCallbacks
private void completeFutureAndFireCallbacks(long baseOffset, long logAppendTime, RuntimeException exception) {
// Set the future before invoking the callbacks as we rely on its state for the `onCompletion` call
produceFuture.set(baseOffset, logAppendTime, exception);
// execute callbacks
for (Thunk thunk : thunks) {
try {
// 判断异常是否为空
if (exception == null) {
RecordMetadata metadata = thunk.future.value();
if (thunk.callback != null)
thunk.callback.onCompletion(metadata, null);
} else {
if (thunk.callback != null)
thunk.callback.onCompletion(null, exception);
}
} catch (Exception e) {
log.error("Error executing user-provided callback on message for topic-partition '{}'", topicPartition, e);
}
}
produceFuture.done();
}
消息发送成功时,metadata不为null而exception为null;反之metadata为null而exception不为null。
如果有地方有疑惑或者写的有不好,可以评论或者通过邮箱联系我creazycoder@sina.com
相关参考: 《深入理解Kafka核心设计与实践原理》