说明
在上篇博文《springboot学习(十七):了解spring - kafka配置工作原理》中,我们简单了解了spring-kafka的配置工作原理。通过源码可以看到spring在实现并发消费时,采用的是线程封闭的策略,也就是一个groupid中,根据配置的concurrency来创建多个消费者线程,每个消费者消费一个或多个分区,来实现整个topic消息的消费处理。本篇博文将对上篇博文中最后提出的问题 ----- 如何对单个分区实现并发消费? 通过代码演示,解答该问题。
正文
spring的线程封闭策略
在spring中实现消息的并发消费采用的是线程封闭的策略,具体实现是在创建监听器容器时,会根据配置的concurrency来创建多个KafkaMessageListenerContainer,在该类中又有内部类ListenerConsumer,在该内部类中封闭创建了consumer对象。以此来实现主题消息的并发消费。
注意,以这种方式进行并发消费时,实际的并发度受到了主题分区数的限制,当消费线程数大于分区数时,会使多出来的消费者线程一直处于空闲状态。对此,spring在创建KafkaMessageListenerContainer前,对用户配置的concurrency值进行了校验,当该值超出主题分区数时,将值设置为实际的分区数。
TopicPartitionInitialOffset[] topicPartitions = containerProperties.getTopicPartitions();
if (topicPartitions != null && this.concurrency > topicPartitions.length) {
this.logger.warn("When specific partitions are provided, the concurrency must be less than or equal to the number of partitions; reduced from " + this.concurrency + " to " + topicPartitions.length);
this.concurrency = topicPartitions.length;
}
同时,以线程封闭的方式实现并发消费,每个消费者线程都需要保持一个TCP连接,如果分区数很大,则会带来很大的系统开销。但是,以该方式实现并发消费,可以保证每个分区消息的顺序消费。
通过KafkaConsumer的消费模式我们可以看到,消费者需要不断的从服务器拉取(poll)消息进行处理,如果消息处理的速度越快,则拉取的频次越高,整体的消费能力越强。所以整体的消费速度在于消息处理模块的速度。我们可以将这个模块改为多线程的处理方式,来提高整体的消费能力。
多线程处理
通过将消息拉取动作和处理动作分开,将处理模块改为多线程的处理方式来提升消息的处理速度,进而提升整体的消费能力。
以上图示,将主题对应的消费者组进行池化,每个group对应一个consumer线程池,池中线程数为主题的分区数。每个消费者是个线程,提交到consumer线程池后,不断从服务器拉取消息,同时在消费者线程中,又有一个用来实际处理消息的MessageHandler线程池,在获取的消息后,根据每批次的消息创建MessagedoHandle线程,提交到handler线程池进行消息的实际处理。
KafaManager
依据以上原理创建KafaManager类,通过该类可以进行消息的发送和消费者的订阅。KafkaProducer是线程安全的,所以在该类中依据配置创建一个单例的KafkaProducer对象。对于消费者的订阅,通过该类提供的subscribe方法,用户可以自定义消息的处理方式,要订阅的主题,groupid,clientid,消息处理线程池的线程数等参数。