简介

Kafka目前主要作为一个分布式的发布订阅式的消息系统使用,也是目前最流行的消息队列系统之一。因此,也越来越多的框架对kafka做了集成,比如本文将要说到的spring-kafka。

Kafka既然作为一个消息发布订阅系统,就包括消息生成者和消息消费者。本文主要讲述的spring-kafka框架的kafkaListener注解的深入解读和使用案例。

解读

源码解读

@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE })

@Retention(RetentionPolicy.RUNTIME)

@MessageMapping

@Documented

@Repeatable(KafkaListeners.class)

public @interface KafkaListener {

   /**

    * 消费者的id,当GroupId没有被配置的时候,默认id为GroupId

    */

   String id() default "";

   /**

    * 监听容器工厂,当监听时需要区分单数据还是多数据消费需要配置containerFactory      属性

    */

   String containerFactory() default "";

   /**

    * 需要监听的Topic,可监听多个,和 topicPattern 属性互斥
*/ String[] topics() default {}; /** * 需要监听的Topic的正则表达。和 topics,topicPartitions属性互斥
*/ String topicPattern() default ""; /** * 可配置更加详细的监听信息,必须监听某个Topic中的指定分区,或者从offset为200的偏移量开始监听,可配置该参数, 和 topicPattern 属性互斥
*/ TopicPartition[] topicPartitions() default {}; /** *侦听器容器组 */ String containerGroup() default ""; /** * 监听异常处理器,配置BeanName */ String errorHandler() default ""; /** * 消费组ID */ String groupId() default ""; /** * id是否为GroupId */ boolean idIsGroup() default true; /** * 消费者Id前缀 */ String clientIdPrefix() default ""; /** * 真实监听容器的BeanName,需要在 BeanName前加 "__" */ String beanRef() default "__listener"; }

使用案例

ConsumerRecord类消费

使用ConsumerRecord类接收有一定的好处,ConsumerRecord类里面包含分区信息、消息头、消息体等内容,如果业务需要获取这些参数时,使用ConsumerRecord会是个不错的选择。如果使用具体的类型接收消息体则更加方便,比如说用String类型去接收消息体。

这里我们编写一个Listener方法,监听"topic1"Topic,并把ConsumerRecord里面所包含的内容打印到控制台中:

@Component

public class Listener {

    private static final Logger log = LoggerFactory.getLogger(Listener.class);

    @KafkaListener(id = "consumer", topics = "topic1")

    public void consumerListener(ConsumerRecord<Integer, String> record) {

        log.info("topic.quick.consumer receive : " + record.toString());

    }

}

批量消费

批量消费在现实业务场景中是很有实用性的。因为批量消费可以增大kafka消费吞吐量,提高性能。

批量消费实现步骤:

1、重新创建一份新的消费者配置,配置为一次拉取10条消息

2、创建一个监听容器工厂,命名为:batchContainerFactory,设置其为批量消费并设置并发量为5,这个并发量根据分区数决定,必须小于等于分区数,否则会有线程一直处于空闲状态。

3、创建一个分区数为8的Topic。

4、创建监听方法,设置消费id为“batchConsumer”,clientID前缀为“batch”,监听“batch”,使用“batchContainerFactory”工厂创建该监听容器。

@Component

public class BatchListener {

    private static final Logger log= LoggerFactory.getLogger(BatchListener.class);

    private Map<String, Object> consumerProps() {

        Map<String, Object> props = new HashMap<>();

        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");

        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);

        props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");

        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "15000");

        //一次拉取消息数量

        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "10");

        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,

                NumberDeserializers.IntegerDeserializer.class);

        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,

                StringDeserializer.class);

        return props;

    }

    @Bean("batchContainerFactory")

    public ConcurrentKafkaListenerContainerFactory listenerContainer() {

        ConcurrentKafkaListenerContainerFactory container

                = new ConcurrentKafkaListenerContainerFactory();

        container.setConsumerFactory(new DefaultKafkaConsumerFactory(consumerProps()));

        //设置并发量,小于或等于Topic的分区数

        container.setConcurrency(5);

        //必须 设置为批量监听

        container.setBatchListener(true);

        return container;

    }

    @Bean

    public NewTopic batchTopic() {

        return new NewTopic("topic.batch", 8, (short) 1);

    }

    @KafkaListener(id = "batchConsumer",clientIdPrefix = "batch"

            ,topics = {"topic.batch"},containerFactory = "batchContainerFactory")

    public void batchListener(List<String> data) {

        log.info("topic.batch  receive : ");

        for (String s : data) {

            log.info(  s);

        }

    }

}

监听Topic中指定的分区

使用@KafkaListener注解的topicPartitions属性监听不同的partition分区。

@TopicPartition:topic--需要监听的Topic的名称,partitions --需要监听Topic的分区id。

partitionOffsets --可以设置从某个偏移量开始监听,@PartitionOffset:partition --分区Id,非数组,initialOffset --初始偏移量。

@Bean

public NewTopic batchWithPartitionTopic() {

    return new NewTopic("topic.batch.partition", 8, (short) 1);

}

@KafkaListener(id = "batchWithPartition",clientIdPrefix = "bwp",containerFactory = "batchContainerFactory",

        topicPartitions = {

                @TopicPartition(topic = "topic.batch.partition",partitions = {"1","3"}),

                @TopicPartition(topic = "topic.batch.partition",partitions = {"0","4"},

                        partitionOffsets = @PartitionOffset(partition = "2",initialOffset = "100"))

        }

)

public void batchListenerWithPartition(List<String> data) {

    log.info("topic.batch.partition  receive : ");

    for (String s : data) {

        log.info(s);

    }

}

注解方式获取消息头及消息体

当你接收的消息包含请求头,以及你监听方法需要获取该消息非常多的字段时可以通过这种方式。。这里使用的是默认的监听容器工厂创建的,如果你想使用批量消费,把对应的类型改为List即可,比如List<String> data , List<Integer> key。

@Payload:获取的是消息的消息体,也就是发送内容

@Header(KafkaHeaders.RECEIVED_MESSAGE_KEY):获取发送消息的key

@Header(KafkaHeaders.RECEIVED_PARTITION_ID):获取当前消息是从哪个分区中监听到的

@Header(KafkaHeaders.RECEIVED_TOPIC):获取监听的TopicName

@Header(KafkaHeaders.RECEIVED_TIMESTAMP):获取时间戳

@KafkaListener(id = "params", topics = "topic.params")

public void otherListener(@Payload String data,

                         @Header(KafkaHeaders.RECEIVED_MESSAGE_KEY) Integer key,

                         @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition,

                         @Header(KafkaHeaders.RECEIVED_TOPIC) String topic,

                         @Header(KafkaHeaders.RECEIVED_TIMESTAMP) long ts) {

    log.info("topic.params receive : \n"+

            "data : "+data+"\n"+

            "key : "+key+"\n"+

            "partitionId : "+partition+"\n"+

            "topic : "+topic+"\n"+

            "timestamp : "+ts+"\n"

    );

}

使用Ack机制确认消费

Kafka是通过最新保存偏移量进行消息消费的,而且确认消费的消息并不会立刻删除,所以我们可以重复的消费未被删除的数据,当第一条消息未被确认,而第二条消息被确认的时候,Kafka会保存第二条消息的偏移量,也就是说第一条消息再也不会被监听器所获取,除非是根据第一条消息的偏移量手动获取。Kafka的ack 机制可以有效的确保消费不被丢失。因为自动提交是在kafka拉取到数据之后就直接提交,这样很容易丢失数据,尤其是在需要事物控制的时候。

使用Kafka的Ack机制比较简单,只需简单的三步即可:

  1. 设置ENABLE_AUTO_COMMIT_CONFIG=false,禁止自动提交
  2. 设置AckMode=MANUAL_IMMEDIATE
  3. 监听方法加入Acknowledgment ack 参数

4.使用Consumer.seek方法,可以指定到某个偏移量的位置

@Component

public class AckListener {

    private static final Logger log = LoggerFactory.getLogger(AckListener.class);

    private Map<String, Object> consumerProps() {

        Map<String, Object> props = new HashMap<>();

        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");

        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

        props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");

        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "15000");

        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);

        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

        return props;

    }

    @Bean("ackContainerFactory")

    public ConcurrentKafkaListenerContainerFactory ackContainerFactory() {

        ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory();

        factory.setConsumerFactory(new DefaultKafkaConsumerFactory(consumerProps()));

        factory.getContainerProperties().setAckMode(AbstractMessageListenerContainer.AckMode.MANUAL_IMMEDIATE);

        factory.setConsumerFactory(new DefaultKafkaConsumerFactory(consumerProps()));

        return factory;

    }

    @KafkaListener(id = "ack", topics = "topic.ack", containerFactory = "ackContainerFactory")

    public void ackListener(ConsumerRecord record, Acknowledgment ack) {

        log.info("topic.quick.ack receive : " + record.value());

        ack.acknowledge();

    }

}

解决重复消费

上一节中使用ack手动提交偏移量时,假如consumer挂了重启,那它将从committed offset位置开始重新消费,而不是consume offset位置。这也就意味着有可能重复消费。

在0.9客户端中,有3种ack策略:

策略1: 自动的,周期性的ack。

策略2:consumer.commitSync(),调用commitSync,手动同步ack。每处理完1条消息,commitSync 1次。

策略3:consumer. commitASync(),手动异步ack。、

那么使用策略2,提交每处理完1条消息,就发送一次commitSync。那这样是不是就可以解决“重复消费”了呢?如下代码:

while (true) {

        List<ConsumerRecord> buffer = new ArrayList<>();

        ConsumerRecords<String, String> records = consumer.poll(100);

        for (ConsumerRecord<String, String> record : records) {

            buffer.add(record);

        }

        insertIntoDb(buffer);    //消除处理,存到db

        consumer.commitSync();   //同步发送ack

        buffer.clear();

    }

}

答案是否定的!因为上面的insertIntoDb和commitSync做不到原子操作:如果在数据处理完成,commitSync的时候挂了,服务器再次重启,消息仍然会重复消费。

那么如何解决重复消费的问题呢?答案是自己保存committed offset,而不是依赖kafka的集群保存committed offset,把消息的处理和保存offset做成一个原子操作,并且对消息加入唯一id,进行判重。

依照官方文档,要自己保存偏移量,需要:

  1. enable.auto.commit=false, 禁用自动ack。
  2. 每次取到消息,把对应的offset存下来。
  3. 下次重启,通过consumer.seek函数,定位到自己保存的offset,从那开始消费。
  4. 更进一步处理可以对消息加入唯一id,进行判重。

spring-kafka之KafkaListener注解深入解读的更多相关文章

  1. Spring Kafka和Spring Boot整合实现消息发送与消费简单案例

    本文主要分享下Spring Boot和Spring Kafka如何配置整合,实现发送和接收来自Spring Kafka的消息. 先前我已经分享了Kafka的基本介绍与集群环境搭建方法.关于Kafka的 ...

  2. Spring Kafka整合Spring Boot创建生产者客户端案例

    每天学习一点点 编程PDF电子书.视频教程免费下载:http://www.shitanlife.com/code 创建一个kafka-producer-master的maven工程.整个项目结构如下: ...

  3. Docker部署Kafka以及Spring Kafka操作

    从https://hub.docker.com/ 查找kafka 第三个活跃并stars数量多 进去看看使用 我们使用docker-compose来构建镜像 查看使用文档中的docker-compos ...

  4. [Spring框架]Spring开发实例: XML+注解.

    前言: 本文为自己学习Spring记录所用, 文章内容包括Spring的概述已经简单开发, 主要涉及IOC相关知识, 希望能够对新入门Spring的同学有帮助, 也希望大家一起讨论相关的知识. 一. ...

  5. 使用 Spring 2.5 基于注解驱动的 Spring MVC

    http://www.ibm.com/developerworks/cn/java/j-lo-spring25-mvc/ 概述 继 Spring 2.0 对 Spring MVC 进行重大升级后,Sp ...

  6. 使用 Spring 2.5 基于注解驱动的 Spring MVC--转

    概述 继 Spring 2.0 对 Spring MVC 进行重大升级后,Spring 2.5 又为 Spring MVC 引入了注解驱动功能.现在你无须让 Controller 继承任何接口,无需在 ...

  7. Spring MVC 中采用注解方式 Action中跳转到另一个Action的写法

    Spring MVC 中采用注解方式 Action中跳转到另一个Action的写法 在Action中方法的返回值都是字符串行,一般情况是返回某个JSP,如: return "xx" ...

  8. JavaEE开发之Spring中的条件注解组合注解与元注解

    上篇博客我们详细的聊了<JavaEE开发之Spring中的多线程编程以及任务定时器详解>,本篇博客我们就来聊聊条件注解@Conditional以及组合条件.条件注解说简单点就是根据特定的条 ...

  9. spring中aop的注解实现方式简单实例

    上篇中我们讲到spring的xml实现,这里我们讲讲使用注解如何实现aop呢.前面已经讲过aop的简单理解了,这里就不在赘述了. 注解方式实现aop我们主要分为如下几个步骤(自己整理的,有更好的方法的 ...

随机推荐

  1. jmeter4.0,启动jmeter.bat闪退问题

    问题描述: 电脑重装win10系统,配置好了java环境后,解压jmeter的zip包,然后按照网上的教程配置环境变量,然后兴冲冲启动jmeter.bat,结果,闪退,甚至连个报错信息都没有... 然 ...

  2. Fiddler手机端抓包环境设置与过滤(一)

    一.PC端Fiddler设置 1.安装https 证书 打开Fiddler->Tool->Fiddler Options->HTTPS tab,勾选上并Capture HTTPS C ...

  3. 面试被问为什么使用Spring Boot?答案好像没那么简单

    面试官:项目中有使用Spring Boot吗? 小小白:用过. 面试官:说一下为什么要使用Spring Boot? 小小白:在使用Spring框架进行开发的过程中,需要配置很多Spring框架包的依赖 ...

  4. CSS躬行记(9)——网格布局

    网格布局(Grid Layout)也叫栅格布局,与表格布局类似,也依赖行和列.但与之不同的是,网格布局能直接控制HTML文档中元素的顺序.位置和大小等,而不用再借助辅助元素. 一.术语 下图展示了CS ...

  5. Day_11【集合】扩展案例1_遍历打印学生信息,获取学生成绩的最高分,获取成绩最高的学员,获取学生成绩的平均值,获取不及格的学员数量

    分析以下需求,并用代码实现: 1.按照以下描述完成类的定义 学生类 属性: 姓名name 年龄age 成绩score 行为: 吃饭eat() study(String content)(content ...

  6. Linux之V4L2基础编程

    Linux之V4L2基础编程 本文内容来源于网络,本博客进行整理. 1. 定义 V4L2(Video For Linux Two) 是内核提供给应用程序访问音.视频驱动的统一接口. 2. 工作流程: ...

  7. git使用-忽略文件更新的几种方法

    有几种情况我们不希望本地文件在 git 里面得到更新. 一.情况:始终不需要git跟踪本地的一些文件 方法:使用.gitignore文件忽略 解释: 使用git init操作创建git控制管理之后,默 ...

  8. 云小课 | WAF反爬虫“三板斧”:轻松应对网站恶意爬虫

    描述:反爬虫是一个复杂的过程,针对爬虫常见的行为特征,WAF反爬虫三板斧——Robot检测(识别User-Agent).网站反爬虫(检查浏览器合法性)和CC攻击防护(限制访问频率)可以全方位帮您解决业 ...

  9. 利用 Maven 构造 Spring Cloud 微服务架构 模块使用 spring Boot构建

    采用Maven 聚合工程搭建 Spring Cloud 使用工具: IntelliJ IDEA    版本: 2019.2.2 maven             版本: 3.6.0 JDK      ...

  10. Selenium + Python + Chrome 自动化测试 环境搭建

    一.下载Python 相关的教程很多,此处不详细记录了,下面是官网下载地址: https://www.python.org/downloads/ 我使用的python版本为 Python 3.6.1 ...