場景
在使用spring-kafka
進行功能開發的時候,思考過這樣一個問題:假如使用信號量的方式(不瞭解的,可以點擊我的這篇文章《Java學習系列:使用SignalHandler來處理Linux信號量,控制程序結束的步驟》 進行了解)來終止程序,雖然我們使用了kafkaTemplate.send
方法發送了,但是假如程序在發送過程就關閉了,是否就會造成數據丟失?即我們調用了kafkaTemplate.send
方法發送了數據,認爲數據已經發送了;但是程序關閉的時候,導致數據未發送成功,進而導致了數據丟失情況的發生。
環境
軟件 | 版本 |
---|---|
JDK | 8 |
Kafka | 2.0.1 |
spring-boot | 2.1.8.RELEASE |
正文
spring-kafka發送消息的介紹
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<SendResult<K, V>> doSend(final ProducerRecord<K, V> producerRecord)
,下面貼一下這個方法的內部實現:
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
的文檔和對應的源碼,實現了消息發送確認的機制:添加對應的回調函數
,等待消息發送完畢之後,調用回調函數進行記錄。從源碼當中,也學到了不少的東西和比較好的設計理念。
參考鏈接
隨緣求贊
如果我的文章對大家產生了幫忙,可以在文章底部點個贊或者收藏;
如果有好的討論,可以留言;
如果想繼續查看我以後的文章,可以左上角點擊關注