如图创建了一个名字为
foo
分区数为3
的topic,在以上情况下,探讨多线程开发Consumer
的方案.
worker线程池模式
该种模式一个
ConsumerGroup
有一个或多个Consumer实例,不断的从订阅的partition
中poll
消息,将获取到的消息投递到workerThreadPool
线程池。
优点
- 将任务切分成了消息获取和消息处理两个部分,分别由不同的线程处理它们。最大优势就在于它的高伸缩性,就是说我们可以独立地调节消息获取的线程数,以及消息处理的线程数,而不必考虑两者之间是否相互影响。如果你的消费获取速度慢,那么增加消费获取的线程数即可;如果是消息的处理速度慢,那么增加 Worker 线程池线程数即可。
缺点
- 实现起来比较困难相当于有消息获取和消息处理两个线程组。
- 该方案将消息获取和消息处理分开了,也就是说获取某条消息的线程不是处理该消息的线程,因此无法保证分区内的消费顺序。这也是他致命的缺点
- 由于缺点2,可能导致消息重复消费问题以及消息丢失问题。
多Consumer模式
消费者程序启动多个线程,每个线程维护专属的 KafkaConsumer 实例,负责完整的消息获取、消息处理流程。
优点
- 实现起来很简单,只需要创建多个Consumer实例数即可
- 线程间是隔离的,不需要去额外的处理线程间交互问题
- 因为每个线程消费的分区是固定的,所以可以很好保证唯一提交的正确性,只要处理的到位,不会出现消息重复,消息丢失问题
缺点
- 因为每一个Consumer实例都相当于一个
TCP
链接,所以对应的资源消耗比较大 - Consumer的实例数受限于
partition
数,Consumer的实例数只能小于等于partition
数。
因为worker线程池模式不能保证消息被成功消费,所以这里强烈建议使用多Consumer模式
编码实现
- Worker线程池模式
...
private KafkaConsumer<String, Object> kafkaConsumer;
private final ExecutorService workers = Executors.newFixedThreadPool(4);
public void run() {
kafkaConsumer.subscribe(Arrays.asList("foo","bar"));
ConsumerRecords<String, Object> records = kafkaConsumer.poll(Duration.ofMillis(100));
records.forEach(record->{
workers.submit(()->handlerRecord(record));
});
}
- 多Consumer实例模式
public class KafkaConsumerWorker implements Runnable {
private final KafkaConsumer<String, Object> consumer;
private final AtomicBoolean closed = new AtomicBoolean(false);
public KafkaConsumerWorker(KafkaConsumer<String, Object> consumer) {
this.consumer = consumer;
}
@Override
public void run() {
while (!closed.get()) {
try {
ConsumerRecords<String, Object> records = consumer.poll(Duration.ofMillis(100));
// todo handler records
} catch (WakeupException e) {
// Ignore exception if closing
if (!closed.get()) {
throw e;
}
} finally {
consumer.close();
}
}
}
public void shutdown() {
closed.set(true);
consumer.wakeup();
}
}