在kakfa相关面试中经常会问到向kafka发送消息是同步发送还是异步发送?这个问题,其实对于kafka生产者来说有三种发送模式发后既忘(fire-and-foget),同步(sync)及异步(async)。我们先解释一下同步和异步:
同步or异步or发后即忘
所谓同步就是线程阻塞等待消息生产的结果,有可能消息发送成功也可能消息发送出异常了,不管怎么样线程都会等待结果返回后才会继续往下走。这种场景适合于我需要确定发送消息成功后再进行下面的操作,比如,我要把订单成功的状态发给消息队列再把订单状态保存到订单库里。这样我就必须要保证往消息队列发送的消息是成功的然后在写库,否则会造成与其他业务线的数据不一致。虽然一致性保证了,但是吞吐量必然受影响,因为线程会阻塞。
而异步是线程不去等待结果返回继续往下进行,而对结果的处理是通过一个回调方法去处理返回结果。适合数据一致要求不高的场景。这样的设计一致性虽然不高,但是吞吐量会得到提升。
大家可能奇怪发后既忘(fire-and-foget)是个什么东东呢?
发后即忘类似异步,区别是并没有异步处理结果的回调方法。就是说只管发不管发送的结果如何,这种适合发日志等不太重要的场景。这样做提吞吐量是最高的。
下面对这三种发送方式在吞吐量和一致性上做了一个比较:
| 同步发送 | 异步发送 | 发后即忘 | |
|---|---|---|---|
| 吞吐量 | 最差 | 中等 | 最高 |
| 一致性 | 最好 | 中等 | 最差 |
kafka是怎样实现的?
那么,kafka是怎样实现上述发送方式的呢?
我们先看同步是怎么做的:
kafka同步发送的实现
KafkaProducer类 的send方法并非是void类型,而是Future类型,方法有2个重载方法,具体定义如下:
public Future send(ProducerRecord<k, V> record)
public Future send(ProducerRecord<k, v> record,Callback callback)
要实现同步的发送方式,可以利用返回的Future对象实现:
try{
producer.send(record).get();
} catch (ExecutionException | InterruptedException e){
e.printStackTrace();
}
实际上send)方法本身就是异步的,send)方法返回的Future对象可以使调用方稍后获得发送的结果。示例中在执行send()方法之后直接链式调用了get()方法来阻塞等待 Kafka的响应直到消息发送成功,或者发生异常。如果发生异常,那么就需要捕获异常并交由外层逻辑处理。
那么我们怎么能知道kafka服务端返回的响应呢?这时我们可以这么做:
可以在执行完send()方法之后不直接调用get()方法,比如下面的一种同步发送方式的实现
try(
Future<RecordMetadata> future=producer.send(record);
RecordMetadata metadata = future.get();
System.out.println(metadata.topic() + "_" +
metadata.partition() +":" + metadata.offset());
}
catch(excutionException | InterruptedException e){
e.printStackTrace();
}
metadata是返回的响应信息。这样我们就可以根据返回响应的详细信息来做一些判断了。
kafka异步发送的实现
我们再来了解一下异步发送的方式,一般是在send()方法里指定一个Callback 的回调函数 Kafka 在返回响应时调用该函数来实现异步的发送确认。
Callback的方式非常洁明了,Kafka 有响应时就会回调。发送成功,或抛出异常。异步发送方式的示例如下
producer.send(record, new Callback() {
@Override
publie void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception != null)
exception.printstackTrace();
else
system.out.printIn(metadata.topic() + "_" +
metadata.partition() +":" + metadata.offset());
示例代码中遇到异常时(exception!=null)只是做了简单的打印操作,在实际应用中应该使用更加稳妥的方式来处理,比如可以将异常记录以便日后分析,也可以做一定的处理来进行消息重发。onCompletion()方法的两个参数是互斥的,消息发送成功时,metadata不为null而 exception为null;消息发送异常时,metadata为null而exception 不为null。
producer.send(record1, callbackl);
producer.send(record2,callback2);
对于同一个分区而言,如果消息record1于record2之前先发送(参考上面的示例代码),那么KafkaProducer 就可以保证对应的callback1在callback2之前调用,也就是说,回调函数的调用也可以保证分区有序。
kafka发后即忘的实现
其实很简单,如下所示:
try{
producer.send(record);
} catch (ExecutionException | InterruptedException e){
e.printStackTrace();
}
发了以后什么都不做就ok了,呵呵很简单吧,就是无法知道是否发送成功,因为根本不会处理服务端的响应。