在使用spring-kafka
进行功能开发的时候,思考过这样一个问题:假如使用信号量的方式(不了解的,可以点击我的这篇文章《Java学习系列:使用SignalHandler来处理Linux信号量,控制程序结束的步骤》 进行了解)来终止程序,虽然我们使用了kafkaTemplate.send
方法发送了,但是假如程序在发送过程就关闭了,是否就会造成数据丢失?即我们调用了kafkaTemplate.send
方法发送了数据,认为数据已经发送了;但是程序关闭的时候,导致数据未发送成功,进而导致了数据丢失情况的发生。
软件 | 版本 |
---|---|
JDK | 8 |
Kafka | 2.0.1 |
spring-boot | 2.1.8.RELEASE |
spring-kafka
提供了几种消息处理的机制,具体的方法如下:
ListenableFuture<SendResult<K, V>> send(String topic, V data);
ListenableFuture<SendResult<K, V>> send(String topic, K key, V data);
ListenableFuture<SendResult<K, V>> send(String topic, Integer partition, K key, V data);
ListenableFuture<SendResult<K, V>> send(String topic, Integer partition, Long timestamp, K key, V data);
ListenableFuture<SendResult<K, V>> send(ProducerRecord<K, V> record);
ListenableFuture<SendResult<K, V>> send(Message<?> message);
这些是从类org.springframework.kafka.core.KafkaOperations
里面截取出来的,而KafkaTemplate
是继承了KafkaOperations
,并实现了里面的方法。如果细心的人可以发现,其实每个方法基本都会返回一个ListenableFuture
对象。通过查看KafkaTemplate
的源码,我们可以看到里面的多个send
方法都会指向protected ListenableFuture
,下面贴一下这个方法的内部实现:
protected ListenableFuture<SendResult<K, V>> doSend(final ProducerRecord<K, V> producerRecord) {
if (this.transactional) {
Assert.state(inTransaction(),
"No transaction is in process; "
+ "possible solutions: run the template operation within the scope of a "
+ "template.executeInTransaction() operation, start a transaction with @Transactional "
+ "before invoking the template method, "
+ "run in a transaction started by a listener container when consuming a record");
}
final Producer<K, V> producer = getTheProducer();
if (this.logger.isTraceEnabled()) {
this.logger.trace("Sending: " + producerRecord);
}
final SettableListenableFuture<SendResult<K, V>> future = new SettableListenableFuture<>();
producer.send(producerRecord, buildCallback(producerRecord, producer, future));
if (this.autoFlush) {
flush();
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Sent: " + producerRecord);
}
return future;
}
从上面的源码,我们可以知道,处理之后,是返回了SettableListenableFuture
对象。那么,这个对象是做什么用的呢?我们可以看一下里面的类图:
从上面的类图,我们可以发现,其实都是继承了Futrue
接口。说到这里,就得说一下Future
是什么东西了。Future
接口是在Java 5
被引入,设计初衷是提供一种异步计算,返回一个执行运算结果的引用,当运算结束之后,这个引用将会被返回给调用方。这样就可以将耗时时间比较久的运算先隔离开,自己去做其他运算,等处理好,再来接收结果。所以,我们在使用kafkaTemplate.send
方法的时候,其实是会将数据放到一个队列里面,等待数据累积到一定阶段或者一定的时间,就会将数据发送出去。这样做是为了减少数据传输的次数,加快发送效率。具体的代码可以查看org.apache.kafka.clients.producer.internals.ProducerBatch
。而这个过程耗费的实际是不确定的,所以没有特殊要求的话,我们一般是不做同步阻塞等待消息结果返回,而是使用回调函数的方式,让spring-kafka
发送消息之后,主动调用回调函数,告知我们数据已经发送成功了。那么,该怎么做?这就是接下来的重点了。
我们一般使用两种方式来对消息发送的结果进行处理,一种是同步阻塞等待结果返回,一种是异步等待结果返回。接下来为大家说明两种情况。
发送完,立即调用Future
等待返回,就是同步阻塞。请看代码:
try {
kafkaTemplate.send(topic, partition,
String.valueOf(Math.round(Math.random() * 100)),
message).get(100, TimeUnit.SECONDS);
handleSuccess(data);
}
catch (ExecutionException e) {
handleFailure(data, record, e.getCause());
}
catch (TimeoutException | InterruptedException e) {
handleFailure(data, record, e);
}
发送完,获取对应的ListenableFuture
对象,并添加对应的回调函数,等待消息发送完毕之后,调用该回调函数方法。具体代码如下:
ListenableFuture<SendResult<String,String>> future = kafkaTemplate.send(topic, partition,
String.valueOf(Math.round(Math.random() * 100)),
message);
future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
@Override
public void onSuccess(SendResult<String, String> result) {
log.trace("发送消息成功,发送主题为:",topic);
}
@Override
public void onFailure(Throwable ex) {
log.error("发送消息失败,消息主题为 {},异常消息为 :{}",topic,ex);
handleFailureData(topic, message);
}
});
而实际项目使用中,我是使用第二种方式,这样既不会耽误消息的发送,也不会错过对应的消息结果,比单一的同步阻塞方法要好多了。
通过研究spring-kafka
的文档和对应的源码,实现了消息发送确认的机制:添加对应的回调函数
,等待消息发送完毕之后,调用回调函数进行记录。从源码当中,也学到了不少的东西和比较好的设计理念。
spring-kafka-docs
如果我的文章对大家产生了帮忙,可以在文章底部点个赞或者收藏;
如果有好的讨论,可以留言;
如果想继续查看我以后的文章,可以左上角点击关注