SpringBoot使用消息中间件Kafka

Kafka介绍

Kafka是Apache旗下的一款分布式流媒体平台,Kafka是一种高吞吐量、持久性、分布式的发布订阅的消息队列系统。 

kafka的目标是实现一个为处理实时数据提供一个统一、高吞吐、低延迟的平台。

基本核心概念

Broker

Kafka 集群包含一个或多个服务器,这种服务器被称为broker。broker端不维护数据的消费状态,提升了性能。直接使用磁盘存储,线性读写,速度快,避免了数据在JVM内存和系统内存之间的复制,减少耗性能的创建对象和垃圾回收。

Producer

负责发布消息到 Kafka broker。

Consumer

消息消费者,向Kafka broker读取消息的客户端,consumer从broker中拉取(pull)数据并进行处理。

Topic

每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同的Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上,但是用户只需要指定消息的Topic即可生产或消费数据而不必关系数据存于何处)。

Partition

Partition是物理上的概念,每个Topic包含一个或多个Partition

partition中的每条消息被分配一个递增的ID(offset)。

每个partition都是一个有序的队列,kafka只能保证一个partition中的消息的顺序,不能保证一个topic(多个partition间)的整体顺序。

Consumer Group

每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定则属于默认的group)。

Topic & Partition

Topic在逻辑上可以被认为是一个Queue,每条消费都必须指定它的Topic,可以理解为必须指明这条消息放进哪个Queue里。为了使得Kafka的吞吐率可以线性提高,物理上把Topic分为一个或多个Partition,每个Partition在物理上对应一个文件夹,该文件夹下存储这个Partition的所有消息和索引文件。

Offset

每条数据都有一个offset,是数据在该partition中的唯一标识(消息的索引号)。

各个consumer会保存其消费到的offset位置,这样下次可以从该offset位置继续消费;consumer消费的offset保存在一个专门的topic中(_consumer_offsets)。

Kafka Consumer与Topic的关系

本质上kafka只支持Topic。

(1)每个group中可以有多个consumer,每个consumer属于一个consumer group

通常情况下,一个group中会包含多个consumer,这样不仅可以提高topic中消息的并发消费能力,而且还能提高"故障容错"性,如果group中的某个consumer失效那么其消费的partitions将会被其他consumer自动接管。

2)对于Topic中的一条特定的消息,只会被订阅此Topic的每个group中的其中一个consumer消费,此消息不会发送给一个group的多个consumer

那么一个group中所有的consumer将会交错的消费整个Topic,每个group中consumer消息消费互相独立,我们可以认为一个group是一个"订阅"者。

3)在kafka中,一个partition中的消息只会被group中的一个consumer消费(同一时刻)

一个Topic中的每个partions,只会被一个"订阅者"中的一个consumer消费,不过一个consumer可以同时消费多个partitions中的消息

4)kafka的设计原理决定,对于一个topic,同一个group中不能有多于partitions个数的consumer同时消费,否则将意味着某些consumer将无法得到消息。

5)kafka只能保证一个partition中的消息被某个consumer消费时是顺序的;事实上,从Topic角度来说,当有多个partitions时,消息仍不是全局有序的。

Kafka为什么采用拉取消息的方式

Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息

一些消息系统,如Apache Flume采用了push模式,将消息推送到下游的consumer。这样做有好处也有坏处:由broker决定消息推送的速率,对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是,push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式。

Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull模式下,consumer就可以根据自己的消费能力去决定这些策略Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发)

SpringBoot集成Kafka

基本用法

引入相关依赖

<!-- kafka -->
<!-- 如果报 org.springframework.core.log.LogAccessor 错误,则设为2.3.0 版本以下-->
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
    <!-- <version>2.2.14.RELEASE</version>-->
    <exclusions>
        <exclusion>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.3.3</version>
</dependency>

application.yml配置kafka

server:
  port: 9999

spring:
  # kafka配置
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      # 发生错误后,消息重发的次数。
      retries: 0
      #当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。
      batch-size: 16384
      # 设置生产者内存缓冲区的大小。
      buffer-memory: 33554432
      # 键的序列化方式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      # 值的序列化方式
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
      # acks=0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。
      # acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
      # acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。
      acks: all
      properties:
        linger:
          ms: 2000 #提交延迟
    consumer:
      group-id: zhTestGroup
      # 自动提交的时间间隔 在spring boot 2.X 版本中这里采用的是值的类型为Duration 需要符合特定的格式,如1S,1M,2H,5D
      auto-commit-interval: 1S
      # 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
      # latest(默认值)在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)
      # earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录
      # none:只要有一个分区不存在已提交的offset,就抛出异常;
      auto-offset-reset: latest
      # 是否自动提交偏移量,默认值是true,为了避免出现重复数据和数据丢失,可以把它设置为false,然后手动提交偏移量
      enable-auto-commit: false
      # 键的反序列化方式
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      # 值的反序列化方式
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      # 单次拉取消息的最大条数
      max-poll-records: 200
      properties:
        session:
          timeout:
            ms: 120000 # 消费会话超时时间(超过这个时间 consumer 没有发送心跳,就会触发 rebalance 操作)
        request:
          timeout:
            ms: 18000 # 消费请求的超时时间
    listener:
      # 在侦听器容器中运行的线程数。
      concurrency: 5
      # listner负责ack,每调用一次,就立即commit
      ack-mode: manual_immediate
      # consumer listener topics 不存在时,启动项目就会报错
      missing-topics-fatal: false

KafkaProducer 消息生产者

@Component
public class KafkaProducer {

    @Resource
    private KafkaTemplate<String, Object> kafkaTemplate;

    public void send(String topicName) {
        System.out.println("kafka topicName:" + topicName);
        String mString = "msg:" + new Date();
        ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topicName, mString);
        future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
            @Override
            public void onFailure(Throwable throwable) {
                //发送失败的处理
                System.out.println(topicName + " - 生产者 发送消息失败:" + throwable.getMessage());
            }

            @Override
            public void onSuccess(SendResult<String, Object> stringObjectSendResult) {
                //成功的处理
                System.out.println(topicName + " - 生产者 发送消息成功:" + stringObjectSendResult.toString());
            }
        });
    }
}

消息消费者 KafkaConsumer

@Component
public class KafkaConsumer {

    @KafkaListener(topics = "ccl_topic", groupId = "zhTestGroup")
    public void cclTopic(ConsumerRecord<String, String> record, Acknowledgment ack, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
        System.out.println("线程=" + Thread.currentThread().getId() + ", 数据key=" + record.key() + ", 数据value=" + record.value());
        System.out.println(record);
        //手动提交offset
        ack.acknowledge();
    }


    /**
     * 并发批量下拉数据,手动提交方式避免消息丢失
     *
     * @param list
     * @param ack
     */
    @KafkaListener(topics = "test", groupId = "zhTestGroup")
    public void listen(List<String> list, Acknowledgment ack, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
        System.out.println("本次批量拉取数量:" + list.size() + " 开始消费....");
        List<String> msgList = new ArrayList<>();
        for (String record : list) {
            Optional<?> kafkaMessage = Optional.ofNullable(record);
            // 获取消息
            kafkaMessage.ifPresent(o -> msgList.add(o.toString()));
        }
        if (msgList.size() > 0) {
            for (String msg : msgList) {
                System.out.println("开始消费消息【" + msg + "】");
            }
            // 更新索引
            // updateES(messages);
        }
        //手动提交offset
        ack.acknowledge();
        msgList.clear();
        System.out.println("消费结束");
    }
}

测试验证

@RestController
public class MemberProducerController {

    @Resource
    private KafkaProducer kafkaProducer;

    @RequestMapping("/sendMsg")
    public String sendMsg(String topicName) {
        kafkaProducer.send(topicName);
        return "success";
    }
}

启动项目,访问:

http://localhost:9999/sendMsg?topicName=ccl_topic
http://localhost:9999/sendMsg?topicName=test

生产者

带回调的生产者

KafkaTemplate提供了一个回调方法addCallback,我们可以在回调方法中监控消息是否发送成功 或 失败时做补偿处理,有两种写法。

第一种写法:

@Test
public void testCallback() {
    String message = "测试Callback";
    kafkaTemplate.send("test", message).addCallback(new SuccessCallback<SendResult<String, Object>>() {
        //成功的回调
        @Override
        public void onSuccess(SendResult<String, Object> success) {
            // 消息发送到的topic
            String topic = success.getRecordMetadata().topic();
            // 消息发送到的分区
            int partition = success.getRecordMetadata().partition();
            // 消息在分区内的offset
            long offset = success.getRecordMetadata().offset();
            System.out.println("发送消息成功1:" + topic + "-" + partition + "-" + offset);
        }
    }, new FailureCallback() {
        //失败的回调
        @Override
        public void onFailure(Throwable throwable) {
            System.out.println("发送消息失败1:" + throwable.getMessage());
        }
    });
}

第二种写法:

@Test
public void testCallback2() {
    String message = "测试testCallback2";
    kafkaTemplate.send("test", message).addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
        @Override
        public void onFailure(Throwable throwable) {
            System.out.println("发送消息失败2:" + throwable.getMessage());
        }

        @Override
        public void onSuccess(SendResult<String, Object> result) {
            System.out.println("发送消息成功2:" + result.getRecordMetadata().topic() + "-"
                               + result.getRecordMetadata().partition() + "-" + result.getRecordMetadata().offset());
        }
    });
}

监听器

Kafka提供了ProducerListener 监听器来异步监听生产者消息是否发送成功,我们可以自定义一个kafkaTemplate添加ProducerListener,当消息发送失败我们可以拿到消息进行重试或者把失败消息记录到数据库定时重试。

@Configuration
public class KafkaConfig {

    @Resource
    private ProducerFactory producerFactory;

    @Bean
    public KafkaTemplate<String, Object> kafkaTemplate() {
        KafkaTemplate<String, Object> kafkaTemplate = new KafkaTemplate<String, Object>(producerFactory);
        kafkaTemplate.setProducerListener(new ProducerListener<String, Object>() {
            @Override
            public void onSuccess(ProducerRecord<String, Object> producerRecord, RecordMetadata recordMetadata) {
                System.out.println("发送成功 topic = " + producerRecord.topic() + " ; partition = " + producerRecord.partition() + "; key = " + producerRecord.key() + " ; value=" + producerRecord.value());
            }

            @Override
            public void onError(ProducerRecord<String, Object> producerRecord, Exception exception) {
                System.out.println("发送失败" + "topic = " + producerRecord.topic() + " ; partition = " + producerRecord.partition() + "; key = " + producerRecord.key() + " ; value=" + producerRecord.value());
                System.out.println(exception.getMessage());
            }
        });
        return kafkaTemplate;
    }
}

注意:当我们发送一条消息,既会走 ListenableFutureCallback 回调,也会走ProducerListener回调。

自定义分区器

我们知道,kafka中每个topic被划分为多个分区,那么生产者将消息发送到topic时,具体追加到哪个分区呢?这就是所谓的分区策略,Kafka 为我们提供了默认的分区策略,同时它也支持自定义分区策略。其路由机制为:

  • 若发送消息时指定了分区(即自定义分区策略),则直接将消息append到指定分区;
  • 若发送消息时未指定 patition,但指定了 key(kafka允许为每条消息设置一个key),则对key值进行hash计算,根据计算结果路由到指定分区,这种情况下可以保证同一个 Key 的所有消息都进入到相同的分区;
  • patition 和 key 都未指定,则使用kafka默认的分区策略,轮询选出一个 patition;

我们来自定义一个分区策略,将消息发送到我们指定的partition,首先新建一个分区器类实现Partitioner接口,重写方法,其中partition方法的返回值就表示将消息发送到几号分区

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;

import java.util.Map;

public class CustomizePartitioner implements Partitioner {
    @Override
    public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
        //自定义分区规则,默认全部发送到0号分区
        return 0;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> map) {

    }
}

在application.yml中配置自定义分区器,配置的值就是分区器类的全路径名

spring:
  kafka:
    producer:
      properties:
        partitioner:
          class: com.harvey.demo.config.CustomizePartitioner

事务提交

如果在发送消息时需要创建事务,可以使用 KafkaTemplate 的 executeInTransaction 方法来声明事务:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

spring.kafka配置项

#################consumer的配置参数(开始)#################
#如果'enable.auto.commit'为true,则消费者偏移自动提交给Kafka的频率(以毫秒为单位),默认值为5000。
spring.kafka.consumer.auto-commit-interval;

#当Kafka中没有初始偏移量或者服务器上不再存在当前偏移量时该怎么办,默认值为latest,表示自动将偏移重置为最新的偏移量
#可选的值为latest, earliest, none
spring.kafka.consumer.auto-offset-reset=latest;

#以逗号分隔的主机:端口对列表,用于建立与Kafka群集的初始连接。
spring.kafka.consumer.bootstrap-servers;

#ID在发出请求时传递给服务器;用于服务器端日志记录。
spring.kafka.consumer.client-id;

#如果为true,则消费者的偏移量将在后台定期提交,默认值为true
spring.kafka.consumer.enable-auto-commit=true;

#如果没有足够的数据立即满足“fetch.min.bytes”给出的要求,服务器在回答获取请求之前将阻塞的最长时间(以毫秒为单位)
#默认值为500
spring.kafka.consumer.fetch-max-wait;

#服务器应以字节为单位返回获取请求的最小数据量,默认值为1,对应的kafka的参数为fetch.min.bytes。
spring.kafka.consumer.fetch-min-size;

#用于标识此使用者所属的使用者组的唯一字符串。
spring.kafka.consumer.group-id;

#心跳与消费者协调员之间的预期时间(以毫秒为单位),默认值为3000
spring.kafka.consumer.heartbeat-interval;

#密钥的反序列化器类,实现类实现了接口org.apache.kafka.common.serialization.Deserializer
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer

#值的反序列化器类,实现类实现了接口org.apache.kafka.common.serialization.Deserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer

#一次调用poll()操作时返回的最大记录数,默认值为500
spring.kafka.consumer.max-poll-records;
#################consumer的配置参数(结束)#################
#################producer的配置参数(开始)#################
#procedure要求leader在考虑完成请求之前收到的确认数,用于控制发送记录在服务端的持久化,其值可以为如下:
#acks = 0 如果设置为零,则生产者将不会等待来自服务器的任何确认,该记录将立即添加到套接字缓冲区并视为已发送。在这种情况下,无法保证服务器已收到记录,并且重试配置将不会生效(因为客户端通常不会知道任何故障),为每条记录返回的偏移量始终设置为-1。
#acks = 1 这意味着leader会将记录写入其本地日志,但无需等待所有副本服务器的完全确认即可做出回应,在这种情况下,如果leader在确认记录后立即失败,但在将数据复制到所有的副本服务器之前,则记录将会丢失。
#acks = all 这意味着leader将等待完整的同步副本集以确认记录,这保证了只要至少一个同步副本服务器仍然存活,记录就不会丢失,这是最强有力的保证,这相当于acks = -1的设置。
#可以设置的值为:all, -1, 0, 1
spring.kafka.producer.acks=1

#每当多个记录被发送到同一分区时,生产者将尝试将记录一起批量处理为更少的请求,
#这有助于提升客户端和服务器上的性能,此配置控制默认批量大小(以字节为单位),默认值为16384
spring.kafka.producer.batch-size=16384

#以逗号分隔的主机:端口对列表,用于建立与Kafka群集的初始连接
spring.kafka.producer.bootstrap-servers

#生产者可用于缓冲等待发送到服务器的记录的内存总字节数,默认值为33554432
spring.kafka.producer.buffer-memory=33554432

#ID在发出请求时传递给服务器,用于服务器端日志记录
spring.kafka.producer.client-id

#生产者生成的所有数据的压缩类型,此配置接受标准压缩编解码器('gzip','snappy','lz4'),
#它还接受'uncompressed'以及'producer',分别表示没有压缩以及保留生产者设置的原始压缩编解码器,
#默认值为producer
spring.kafka.producer.compression-type=producer

#key的Serializer类,实现类实现了接口org.apache.kafka.common.serialization.Serializer
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer

#值的Serializer类,实现类实现了接口org.apache.kafka.common.serialization.Serializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer

#如果该值大于零时,表示启用重试失败的发送次数
spring.kafka.producer.retries
#################producer的配置参数(结束)#################
#################listener的配置参数(结束)#################
#侦听器的AckMode,参见https://docs.spring.io/spring-kafka/reference/htmlsingle/#committing-offsets
#当enable.auto.commit的值设置为false时,该值会生效;为true时不会生效
spring.kafka.listener.ack-mode;

#在侦听器容器中运行的线程数
spring.kafka.listener.concurrency;

#轮询消费者时使用的超时(以毫秒为单位)
spring.kafka.listener.poll-timeout;

#当ackMode为“COUNT”或“COUNT_TIME”时,偏移提交之间的记录数
spring.kafka.listener.ack-count;

#当ackMode为“TIME”或“COUNT_TIME”时,偏移提交之间的时间(以毫秒为单位)
spring.kafka.listener.ack-time;
#################listener的配置参数(结束)#################

 

 

 

常用API

SpringBoot 版本:2.1.7.RELEASE

Spring For Apache Kafka 版本:2.2.11.RELEASE

以下用到的一些常量:

public final class KafkaConstants {
    public static String bootstrapServers = "192.168.198.155:9093,192.168.198.155:9094,192.168.198.155:9095";
}

Topic 配置

@Configuration
public class KafkaTopicConfig {

    /**
     * 定义一个KafkaAdmin的bean,可以自动检测集群中是否存在topic,不存在则创建
     */
    @Bean
    public KafkaAdmin kafkaAdmin() {
        Map<String, Object> configs = new HashMap<>();
        // 指定多个kafka集群多个地址,例如:192.168.2.11,9092,192.168.2.12:9092,192.168.2.13:9092
        configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, KafkaConstants.bootstrapServers);
        return new KafkaAdmin(configs);
    }

    /**
     * 创建 Topic
     */
    @Bean
    public NewTopic topicInfo() {
        // 创建topic,需要指定创建的topic的"名称"、"分区数"、"副本数量(副本数数目设置要小于Broker数量)"
        return new NewTopic("test", 3, (short) 0);
    }
}

producer 配置

创建producer配置类

/**
 * 设置@Configuration、@EnableKafka两个注解,声明Config并且打开KafkaTemplate能力。
 */
@Configuration
@EnableKafka
public class KafkaProducerConfig {

    /**
     * Producer Template 配置
     */
    @Bean(name = "kafkaTemplate")
    public KafkaTemplate<String, String> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }

    /**
     * Producer 工厂配置
     */
    @Bean
    public ProducerFactory<String, String> producerFactory() {
        return new DefaultKafkaProducerFactory(producerConfigs());
    }

    /**
     * Producer 参数配置
     */
    @Bean
    public Map<String, Object> producerConfigs() {
        Map<String, Object> props = new HashMap<>();
        // 指定多个kafka集群多个地址
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KafkaConstants.bootstrapServers);

        // 重试次数,0为不启用重试机制
        props.put(ProducerConfig.RETRIES_CONFIG, 0);
        //同步到副本, 默认为1
        // acks=0 把消息发送到kafka就认为发送成功
        // acks=1 把消息发送到kafka leader分区,并且写入磁盘就认为发送成功
        // acks=all 把消息发送到kafka leader分区,并且leader分区的副本follower对消息进行了同步就任务发送成功
        props.put(ProducerConfig.ACKS_CONFIG, 1);

        // 生产者空间不足时,send()被阻塞的时间,默认60s
        props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 6000);
        // 控制批处理大小,单位为字节
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 4096);
        // 批量发送,延迟为1毫秒,启用该功能能有效减少生产者发送消息次数,从而提高并发量
        props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
        // 生产者可以使用的总内存字节来缓冲等待发送到服务器的记录
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 40960);
        // 消息的最大大小限制,也就是说send的消息大小不能超过这个限制, 默认1048576(1MB)
        props.put(ProducerConfig.MAX_REQUEST_SIZE_CONFIG, 1048576);
        // 键的序列化方式
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 值的序列化方式
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 压缩消息,支持四种类型,分别为:none、lz4、gzip、snappy,默认为none。
        // 消费者默认支持解压,所以压缩设置在生产者,消费者无需设置。
        props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "none");
        return props;
    }
}

向kafka中发送数据

@Service
public class KafkaProducerService {

    @Autowired
    private KafkaTemplate kafkaTemplate;

    /**
     * producer 同步方式发送数据
     *
     * @param topic   topic名称
     * @param message producer发送的数据
     */
    public void sendMessageSync(String topic, String message) throws InterruptedException, ExecutionException, TimeoutException {
        kafkaTemplate.send(topic, message).get(10, TimeUnit.SECONDS);
    }

    /**
     * producer 异步方式发送数据
     *
     * @param topic   topic名称
     * @param message producer发送的数据
     */
    public void sendMessageAsync(String topic, String message) {
        kafkaTemplate.send(topic, message).addCallback(new ListenableFutureCallback() {
            @Override
            public void onFailure(Throwable throwable) {
                System.out.println("success");
            }

            @Override
            public void onSuccess(Object o) {
                System.out.println("failure");

            }
        });
    }
}

consumer配置

创建 Consumer 配置类

@Configuration
@EnableKafka
public class KafkaConsumerConfig {

    @Bean
    public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, String>
                factory = new ConcurrentKafkaListenerContainerFactory<>();
        // 设置消费者工厂
        factory.setConsumerFactory(consumerFactory());
        // 消费者组中线程数量
        factory.setConcurrency(3);
        // 拉取超时时间
        factory.getContainerProperties().setPollTimeout(3000);

        // 当使用批量监听器时需要设置为true
        factory.setBatchListener(true);

        return factory;
    }

    @Bean
    public ConsumerFactory<String, String> consumerFactory() {
        return new DefaultKafkaConsumerFactory(consumerConfigs());
    }

    @Bean
    public Map<String, Object> consumerConfigs() {
        Map<String, Object> propsMap = new HashMap<>();
        // Kafka地址
        propsMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KafkaConstants.bootstrapServers);
        //配置默认分组,这里没有配置+在监听的地方没有设置groupId,多个服务会出现收到相同消息情况
        propsMap.put(ConsumerConfig.GROUP_ID_CONFIG, "defaultGroup");
        // 是否自动提交offset偏移量(默认true)
        propsMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
        // 自动提交的频率(ms)
        propsMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "100");
        // Session超时设置
        propsMap.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "15000");
        // 键的反序列化方式
        propsMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        // 值的反序列化方式
        propsMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        // offset偏移量规则设置:
        // (1)、earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
        // (2)、latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
        // (3)、none:topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
        propsMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        return propsMap;
    }
}

创建 Consumer Listener监听 Kafka 数据

@Component
public class KafkaConsumerListener {

   @KafkaListener(topics = {"test"},groupId = "group1", containerFactory="kafkaListenerContainerFactory")
   public void kafkaListener(String message){
       System.out.println(message);
   }

}

过滤监听器中的消息

在接收消息时候可以场景一个过滤器来过滤消息,这样可以方便我们处理不必要的消息,只关心处理我们需要的消息。

在KafkaListenerContainerFactory 中配置一个过滤器

@Bean
KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>> kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String>
        factory = new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    factory.setConcurrency(3);
    factory.getContainerProperties().setPollTimeout(3000);
    // 将过滤器添添加到参数中
    factory.setRecordFilterStrategy(consumerRecord -> {
        // 设置过滤器,只接收消息内容中包含 "test" 的消息
        String value = consumerRecord.value().toString();
        if (value !=null && value.contains("test")) {
            System.err.println(consumerRecord.value());
            // 返回 false 则接收消息
            return false;
        }
        // 返回 true 则抛弃消息
        return true;
    });
    return factory;
}

监听器的异常处理

单消息消费异常处理器

errorHandler 不指定listenErrorHandler的情况,使用全局异常

/**
 * 单消息消费异常处理器
 */
@Service
public class ConsumerService {

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

    /**
     * 消息监听器
     * errorHandler 不指定listenErrorHandler的情况,使用全局异常
     */
    @KafkaListener( topics = {"test"},groupId = "group21",errorHandler = "listenErrorHandler")
    public void listen(String message) {
        log.info(message);
        // 创建异常,触发异常处理器
        throw new NullPointerException("测试错误处理器");
    }

    /**
     * 异常处理器
     */
    @Bean
    public ConsumerAwareListenerErrorHandler listenErrorHandler() {
        return new ConsumerAwareListenerErrorHandler() {

            @Override
            public Object handleError(Message<?> message,
                                      ListenerExecutionFailedException e,
                                      Consumer<?, ?> consumer) {
                log.info("message:" + message.getPayload());
                log.info("exception:" + e.getMessage());
                return null;
            }
        };
    }
}

批量消费异常处理器

批量消费代码也是差不多的,只不过传递过来的数据都是List集合方式。

/**
 *  批量消费异常处理器
 */
@Service
public class ConsumerBatchService {

    private static final Logger log = LoggerFactory.getLogger(ConsumerBatchService.class);
    /**
     * 消息监听器
     */
    @KafkaListener( topics = {"test"},groupId = "group20",errorHandler = "listenErrorHandler")
    public void listen(List<String> messages) {
        for(String msg:messages){
            System.out.println(msg);
        }
        // 创建异常,触发异常处理器
        throw new NullPointerException("测试错误处理器");
    }


    /**
     * 异常处理器
     */
    @Bean
    public ConsumerAwareListenerErrorHandler listenErrorHandler() {
        return new ConsumerAwareListenerErrorHandler() {
            @Override
            public Object handleError(Message<?> message, ListenerExecutionFailedException e, Consumer<?, ?> consumer) {
                log.info("consumerAwareErrorHandler receive : "+message.getPayload().toString());
                MessageHeaders headers = message.getHeaders();
                List<String> topics = headers.get(KafkaHeaders.RECEIVED_TOPIC, List.class);
                List<Integer> partitions = headers.get(KafkaHeaders.RECEIVED_PARTITION_ID, List.class);
                List<Long> offsets = headers.get(KafkaHeaders.OFFSET, List.class);
                Map<TopicPartition, Long> offsetsToReset = new HashMap<>();
                return null;
            }
        };
    }
}

全局异常处理

将异常处理器添加到 kafkaListenerContainerFactory 中来设置全局异常处理。

@Bean
KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>> kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String>
        factory = new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    factory.setConcurrency(3);
    factory.getContainerProperties().setPollTimeout(3000);
    // 将单条消息异常处理器添加到参数中
    factory.setErrorHandler(new ConsumerAwareErrorHandler() {
        @Override
        public void handle(Exception thrownException, ConsumerRecord<?, ?> data, Consumer<?, ?> consumer) {
            log.error("// 将单条消息异常");
        }
    });
    // 将批量消息异常处理器添加到参数中
    factory.setBatchErrorHandler(new BatchErrorHandler() {
        @Override
        public void handle(Exception thrownException, ConsumerRecords<?, ?> data) {
            log.error("// 将批量消息异常");
        }
    });
    return factory;
}

Kafka Consumer 手动/自动提交 Offset

在kafka的消费者中有一个非常关键的机制,那就是 offset 机制。它使得 Kafka 在消费的过程中即使挂了或者引发再均衡问题重新分配 Partation,当下次重新恢复消费时仍然可以知道从哪里开始消费。

Kafka中偏移量的自动提交是由参数 enable_auto_commit 和 auto_commit_interval_ms 控制的,当 enable_auto_commit=true 时,Kafka在消费的过程中会以频率为 auto_commit_interval_ms 向 Kafka 自带的 topic(__consumer_offsets) 进行偏移量提交,具体提交到哪个 Partation 是以算法:”partation=hash(group_id)%50” 来计算的。

在 Spring 中对 Kafka 设置手动或者自动提交Offset如下:

自动提交

自动提交需要配置下面两个参数:

  • auto.commit.enable=true:是否将offset维护交给kafka进行维护(老版本中提交到zookeeper中维护),设置为true。
  • auto.commit.interval.ms=10000:自动提交时间间隔。

手动提交

手动提交需要配置下面一个参数:

auto.commit.enable=false:是否将offset维护交给kafka进行维护(老版本中提交到zookeeper中维护),设置为false。

然后需要在程序中设置ack模式,从而进行手动提交维护offset。

@Bean
KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>> kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    factory.setConcurrency(3);
    factory.getContainerProperties().setPollTimeout(3000);
    //设置ACK模式(手动提交模式,这里有七种)
    factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.RECORD);
    return factory;
}

在 kafkaListenerContainerFactory 配置中设置 AckMode,它有七种模式分别为:

  • RECORD: 每处理完一条记录后提交。
  • BATCH(默认): 每次poll一批数据后提交一次,频率取决于每次poll的调用频率。
  • TIME: 每次间隔ackTime的时间提交。
  • COUNT: 处理完poll的一批数据后并且距离上次提交处理的记录数超过了设置的ackCount就提交。
  • COUNT_TIME: TIME和COUNT中任意一条满足即提交。
  • MANUAL: 手动调用Acknowledgment.acknowledge()后,并且处理完poll的这批数据后提交。
  • MANUAL_IMMEDIATE: 手动调用Acknowledgment.acknowledge()后立即提交。

注意:如果设置 AckMode 模式为 MANUAL 或者 MANUAL_IMMEDIATE,则需要对监听消息的方法中,引入 Acknowledgment 对象参数,并调用 acknowledge() 方法进行手动提交。

 

posted @ 2022-04-24 14:34  残城碎梦  阅读(945)  评论(0编辑  收藏  举报