使用CyclicBarrier控制Kafka多线程消费消息的位移提交问题

Kafka中消费者是线程不安全的,一个topic只能被一个消费组中的消费者消费,想要提高数据消费能力,可以增加分区数,因为消费者数可以和分区数进行对应,当消费者数大于分区数时,多余的消费者将处于空闲状态,或者也可以在每个线程中创建一个消费者实例,这样也可以对数据来处理,但创建多个消费者实例必然会造成资源的浪费。通过线程池来对数据进行消费,就会存在位移提交的问题,从而引发数据丢失或重复,所以对位移的提交要格外处理,消费者默认是定时提交位移信息的,如果需要手动提交,要先修改配置参数关闭自动提交,再通过代码里调用commitSync()方法。

由于多线程的不可控性,如果让每个线程单独来获取数据再提交位移,很有可能就会造成位移错位等问题,如何合理的控制线程之间任务处理和位移提交问题,这里采用CyclicBarrier工具类,它的本质是一种比较特别的锁,通过配置线程数,当到达指定线程数后再统一执行某些操作,这些特性很适合用来控制位移的提交,我们可以将拉取到的数据分配到线程池中,当所有线程都处理完成后,触发CyclicBarrier中的提交任务,进行一次提交,接着再分配下一轮的数据。

以下便是根据这一策略编写的代码,仅供参考,因为尚未在生产环境中使用,只是对这种思路提高一种实现方式,可能还存在些问题,如当这一批拉取的数据小于线程池中线程数该如何等待,会不会存在短板效应,导致其他线程完成后一直等待某个线程执行,造成短时间位移提交阻塞。

总体思路有点类似于滑动窗口,每次一批一批的处理,等待最后一个处理完成,再向前滑动。
public class KafkaMultiThread {

    private static final Logger LOG = LoggerFactory
            .getLogger(KafkaMultiThread.class);

    public static void main(String[] args) {
        //这里创建一个消费者,使用外部的配置文件信息
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(new Properties());
        //订阅主题
        kafkaConsumer.subscribe(Arrays.asList("topic-test"));
        //需要创建的线程数,这里线程池数和CyclicBarrier的数量要一致
        int threadNumber = 10;
        ExecutorService pool = Executors.newFixedThreadPool(threadNumber);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(threadNumber,()->{
            LOG.info("一批数据处理完毕,统一提交位移");
            kafkaConsumer.commitSync();
        });
        //轮询获取消息
        while (true) {
            ConsumerRecords<String, String> records = kafkaConsumer.poll(1000);
            //对拉取到一批数据分别放入线程池中
            for (ConsumerRecord<String, String> record : records) {
                pool.execute(new KafkaHandle(record,cyclicBarrier));
            }
        }

    }
}

/**
 * 单独处理的线程
 */
class KafkaHandle implements Runnable{
    private static final Logger LOG = LoggerFactory
            .getLogger(KafkaHandle.class);

    ConsumerRecord<String, String> record;
    CyclicBarrier cyclicBarrier;

    /**
     * 构造函数初始化
     * @param record 消息内容
     * @param cyclicBarrier 主线程中的CyclicBarrier
     */
    public KafkaHandle(ConsumerRecord<String, String> record, CyclicBarrier cyclicBarrier) {
        this.record = record;
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        //模拟处理已分配的消息
        LOG.info("offset = %d, value = %s", record.offset(), record.value());
        //这里假设需要将数据转换为json
        Map<String, Object> json = JSONUtil.parseObj(record.value());
        LOG.info(String.valueOf(json.size()));
        try {
            //处理完毕后进入等待
            cyclicBarrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}