kafka—快速入门

简介

Kafka 是最初由 Linkedin 公司开发,是一个分布式、支持分区的(partition)、多副本的(replica)。基于 zookeeper 协调的分布式消息系统。
它的最大的特性就是可以实时的处理大量数据以满足各种需求场景:比如基于hadoop的批处理系统、低延迟的实时系统、Storm/Spark 流式处理引擎,web/nginx 日志、访问日志,消息服务等等,用scala语言编写。
Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目。

使用场景

1、日志收集

利用 Kafka 作为中间层收集各种服务的 log 日志,最后将消息发给下游消费者进行效费,例如 hadoop、hbase、soler、es 等。

2、消息中间件

用作消息队列解耦消息生产方和消息解耦方。

3、大数据收集运营监控处理分析

用户活动、使用记录都会收集然后作为消息被各个服务器发送到 kafka 的 Topic 中,订阅者通过订阅这些 Topic 来进行实时的监控和处理分析。

所以对于 Kafka,其实更偏向于 Hadoop 体系的一员,平常对于消息解耦、流量削峰填谷等场景我们更偏向于使用 RabbitMQ、RocketMQ 等消息中间件。

核心概念

先看看 Kafka 的架构图:

  • Broker 消息中间件:一个 Kafka 节点就是一个 Broker,多个 Broker节点构成 Kafka集群;
  • Topic 主题:Kafka 根据消息主题对消息分类然后放到各 Broker 节点上;
  • Partition 分区:单个 Topic 主题可以划分为多个 Parittion分区,每个分区内消息是有序的;消息在被追加到分区日志文件的时候会分配一个特定的偏移量(offset),offset 是消息在分区中的唯一标识,Kafka 通过它来保证分区的顺序性,但是 offset 并不跨越分区,kafka 只保证分区有序而不保证主题有序;
  • Producer 消息生产者:消息由客户端作为生产者发送到 Kafka 集群;
  • Consumer 消息消费者:消费者从 Broker 读取指定 Topic 进行消费;
  • ConsumerGroup 消费者组:单个 Consumer 只属于一个 ConsumerGroup,一个消息可以被多个不同的 ConsumerGroup 消费,一个 ConsumerGroup 中只能有一个 Consumer 消费该消息。
  • 每一条消息发送到 broker 之前会根据分区规则选择具体存储到哪个分区,分区规则设置合理可以使所有消息均匀分派到各分区中。在创建主题的时候可以通过指定的参数来设置分区的个数,通过增加分区数量实现水平扩展。
  • 如图1-3所示,配置了副本因子为3,也就是说每个分区有1个 leader副本和2 个follower副本,生产者和消费者只与 leader 副本交互,follower 副本只负责消息的同步。消费者需要通过 Pull(拉模式)从服务端拉取消息并且保存消息消费的位置,当消费者宕机后恢复上线能够重新拉取消息;
  • 分区中所有副本称为 AR(Assigned Replicas),所有和 leader 副本保持一定程度同步的副本组成了 ISR(In-Sync Replicas),ISR集合是 AR集合的一个子集,与 leader副本滞后过多的副本称为 OSR(Out-of-Sync Replicas),AR=ISR+OSR,正常情况下 OSR集合应该为空。
  • 消费者在读取一个分区时有一定限制,他只能拉取到 HW(High Watermark) 高水位之前的消息,如图1-4所示,第一条消息的offset为0,最后一条消息的offset为8,offset为9的消息用虚线框表示,代表下一条将待写入的消息,其中日志文件的 HW 为 6,说明消费者只能拉取到 offset在 0~5之间的消息,offset 为6的消息对消费者而言是不可见的。
  • LEO(Log End Offset)标识着当前日志文件中下一条待写入消息的 offset,它的值为当前日志分区中最后一条消息的 offset值加1。分区 ISR集合中每个副本都会维护自己的 LEO,而ISR集合中最小的 LEO就是分区的 HW,对于消费者而言只能消费 HW之前的消息。

举个栗子




如图,某个分区的 ISR集合有三个副本,1个 leader副本和 2个follower副本,此时分区的 LEO 和 HW 都为3;当消息3 和消息4 从生产者发出后会先存入 leader副本,此时 follower副本会发送请求从 leader副本拉取消息3 和消息4进行消息同步;假设此时 follower1 完全跟上了 leader的进度,HW=LEO=5,follower2 只同步了部分消息,HW=LEO=4,此时消费者可以消费offset为 0~3 之间的消息。

待所有消息副本都成功写入到了消息3 和消息4,整个分区的HW=LEO=5,因此消费者可以消费 offset=4 的消息。

Kafka 的复制机制既不是完全的同步复制,也不是单纯的异步复制。同步复制要求所有能工作的 follower副本都复制完,这条消息才会被确认已成功提交,严重影响性能;异步复制方式,数据只要被 leader副本写入就认为成功提交了,但是这种情况下 follower副本还没有完成复制,就会造成数据丢失。Kafka 使用的 ISR方式有效地权衡了数据可靠性和性能。

简单使用——单机

Docker 部署

https://www.cnblogs.com/angelyan/p/14445710.html

https://blog.csdn.net/u011374856/article/details/103469260

https://blog.csdn.net/u011374856/category_9512648.html

Docker 部署 zookeeper

  1. 创建子网

因为 kafka 集群需要基于 zookeeper 注册中心实现部署,kafka 和 zookeeper 均使用 docker 部署,为了保证 docker 容器间的网络互通,先创建 docker 的自定义子网。

# 创建子网,桥接模式
docker network create --driver bridge --subnet 172.0.0.0/16 dk_network

# 查看已经存在的网络
docker network ls
  1. 创建文件挂载
mkdir -p /opt/docker/zookeeper_cluster/conf
mkdir -p /opt/docker/zookeeper_cluster/data
mkdir -p /opt/docker/zookeeper_cluster/log
  1. 安装 zookeeper
# 拉取镜像
docker pull wurstmeister/zookeeper

# 先启动容器,将容器中配置文件拷贝出来
docker run -d --name zookeeper_cluster --restart always wurstmeister/zookeeper
docker cp -a zookeeper_cluster:/opt/zookeeper-3.4.13/conf/zoo.cfg /opt/docker/zookeeper_cluster/conf/zoo.cfg

# 删除容器
docker rm -f zookeeper
  1. 重启 zookeeper,加上配置文件挂载
docker run -d --name zookeeper_cluster --restart always \
-p 2181:2181 -p 2888:2888 -p 3888:3888 \
--network dk_network \
--ip 172.0.0.8 \
-v /opt/docker/zookeeper_cluster/conf/zoo.cfg:/conf/zoo.cfg \
-v /opt/docker/zookeeper_cluster/data:/data \
-v /opt/docker/zookeeper_cluster/log:/datalog \
wurstmeister/zookeeper

Docker 部署 kafka

子网上面已经创建完了,可以直接使用:

  1. 安装 kafka
# 拉取 kafka 镜像
docker pull wurstmeister/kafka

docker run -d  -p 9092:9092 \
--name kafka \
--network dk_network \
--ip 172.0.0.10 \
-e KAFKA_BROKER_ID=0 \
-e KAFKA_ZOOKEEPER_CONNECT=172.0.0.8:2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://172.0.0.10:9092 \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \
wurstmeister/kafka:latest

# 拷贝配置文件到主机
docker cp -a kafka:/opt/kafka/config/ /opt/docker/kafka/config

# 删除 kafka 容器
docker rm -f kafka
  1. 重启 kafka,加上文件挂载
docker run -d  -p 9092:9092 \
--name kafka \
--network dk_network \
--ip 172.0.0.10 \
-e KAFKA_BROKER_ID=0 \
-e KAFKA_ZOOKEEPER_CONNECT=172.0.0.8:2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://172.0.0.10:9092 \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \
-v /opt/docker/kafka/config:/opt/kafka/config \
wurstmeister/kafka:latest

kafka 基础命令使用

kafka 文档:https://kafka.apache.org/documentation/

参考:https://blog.csdn.net/w903328615/article/details/113727408

核心配置文件 server.properties

进入 docker 中的 kafka容器,切换到 /opt/kafka,可以查看 kafka 的核心配置文件 server.properties,其中一些关键配置如下:

参数 默认值 描述
broker.id 0 服务器ID,每个节点使用一个唯一的非负整数标识,必须保证唯一
log.dirs /tmp/kafka-logs 存放数据路径,可配置多个,使用英文逗号分隔。每当创建新的 partition 会选择分区最少的路径进行创建
listeners PLAINTEXT://:9092 server 接受客户端连接的端口,ip 配置 kafka 本机 ip 即可,如 PLAINTEXT://localhost:9092
zookeeper.connect localhost:2181 zookeeper 注册中心连接地址,多个使用英文逗号分隔
log.retention.hours 168 每个日志文件删除之前保存的时间。默认数据保存时间对所有 topic 都一样。
num.partitions 1 topic 默认分区数
default.replication.factor 1 自动创建 topic 的默认副本数量,建议设置为大于等于2
min.insync.replicas 1 当 producer 设置 acks 为 -1 时,min.insync.replicas 用于指定 replicas 的最小同步数目(必须确认每一个 repica 的写数据都是成功的),如果这个数目没有达到,producer发送消息会产生异常
delete.topic.enable false 是否允许删除主题

kafka 命令行操作

# 主题操作
# 1.创建主题(当 producer 向一个主题发送消息但是主题不存在时会自动创建)
./bin/kafka-topics.sh --create --zookeeper 172.0.0.8:2181 --replication-factor 1 --partitions 1 --topic dyingGQ
# 2.查看主题
./bin/kafka-topics.sh --list --zookeeper 172.0.0.8:2181
# 3.删除主题
.bin/kafka-topics.sh --delete --topic dyingGQ --zookeeper 172.0.0.8:2181

# 消息发送和消费
# 4.往主题发送消息
./bin/kafka-console-producer.sh --broker-list 172.0.0.10:9092 --topic dyingGQ
# 5.消费对应主题的消息
./bin/kafka-console-consumer.sh --bootstrap-server 172.0.0.10:9092 --topic dyingGQ
# 6.消费所有历史消息
./bin/kafka-console-consumer.sh --bootstrap-server 172.0.0.10:9092 --from-beginning --topic dyingGQ
# 7.多主题消费
./bin/kafka-console-consumer.sh --bootstrap-server 172.0.0.10:9092 --whitelist "dyingGQ|dyingGQ-1|dyingGQ-2"
# 8.单播消费,属于同一个消费者组的消费者,只有一个能够消费消息
# group1 的消费者1
./bin/kafka-console-consumer.sh --bootstrap-server 172.0.0.10:9092  --consumer-property group.id=group1 --topic dyingGQ 
# group1 的消费者2
./bin/kafka-console-consumer.sh --bootstrap-server 172.0.0.10:9092  --consumer-property group.id=group1 --topic dyingGQ 
# 9.多播消费,不同的消费者组可以同时消费同一条消息
./bin/kafka-console-consumer.sh --bootstrap-server 172.0.0.10:9092  --consumer-property group.id=group2 --topic dyingGQ 

# 消费者组状态查看
# 10.查看现有的消费者组名
./bin/kafka-consumer-groups.sh --bootstrap-server 172.0.0.10:9092 --list
# 11.查看消费者组消息的偏移量
./bin/kafka-consumer-groups.sh --bootstrap-server 172.0.0.10:9092 --describe --group group1

# 分区操作
# 12.多分区主题
./bin/kafka-topics.sh --create --zookeeper 172.0.0.10:2181 --replication-factor 1 --partitions 2 --topic dyingGQ1
# 13.查看分区信息
./bin/kafka-topics.sh --describe --zookeeper 172.0.0.10:2181 --topic dyingGQ1
# 14.分区扩容(扩容到3个分区)
./bin/kafka-topics.sh -alter --partitions 3 --zookeeper 172.0.0.10:2181 --topic dyingGQ

基于 docker 的 kafka 集群部署

Docker-Compose 部署kafka集群:https://github.com/wurstmeister/kafka-docker

多个 kafka 集群对应多个 broker 节点,各个节点拥有自己的 IP地址和 brokerID,每个集群节点部署和上面类似,但是需要修改IP地址、映射的kafka服务端端口号,操作如下:

# 创建集群节点 kafka-1
docker run -d  -p 9093:9093 \
--name kafka-1 \
--network dk_network \
--ip 172.0.0.11 \
-e KAFKA_BROKER_ID=0 \
-e KAFKA_ZOOKEEPER_CONNECT=172.0.0.8:2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://172.0.0.11:9093 \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9093 \
wurstmeister/kafka:latest
# 拷贝配置文件到主机
docker cp -a kafka-1:/opt/kafka/config/ /opt/docker/kafka/config-1
# 删除 kafka 容器
docker rm -f kafka-1
# 挂载模式重启
docker run -d  -p 9093:9093 \
--name kafka-1 \
--network dk_network \
--ip 172.0.0.11 \
-e KAFKA_BROKER_ID=1 \
-e KAFKA_ZOOKEEPER_CONNECT=172.0.0.8:2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://172.0.0.11:9093 \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9093 \
-v /opt/docker/kafka/config-1:/opt/kafka/config \
wurstmeister/kafka:latest

# 创建集群节点 kafka-2
docker run -d  -p 9094:9094 \
--name kafka-2 \
--network dk_network \
--ip 172.0.0.12 \
-e KAFKA_BROKER_ID=2 \
-e KAFKA_ZOOKEEPER_CONNECT=172.0.0.8:2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://172.0.0.12:9094 \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9094 \
wurstmeister/kafka:latest
# 拷贝配置文件到主机
docker cp -a kafka-2:/opt/kafka/config/ /opt/docker/kafka/config-2
# 删除 kafka 容器
docker rm -f kafka-2
# 挂载模式重启
docker run -d  -p 9094:9094 \
--name kafka-2 \
--network dk_network \
--ip 172.0.0.12 \
-e KAFKA_BROKER_ID=2 \
-e KAFKA_ZOOKEEPER_CONNECT=172.0.0.8:2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://172.0.0.12:9094 \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9094 \
-v /opt/docker/kafka/config-2:/opt/kafka/config \
wurstmeister/kafka:latest

部署完毕后,登录 zookeeper 查看节点是否配置成功:

# 进入 zookeeper 容器
docker exec -it 7920e7f8638e /bin/bash

# 启动客户端
./bin/zkCli
# 查看对应路径
ls /brokers/ids
# [2, 1, 0] 正确

kafka 集群下的一些操作:

# 集群下创建多分区主题
./bin/kafka-topics.sh --create --zookeeper 172.0.0.8:2181 --replication-factor 3 --partitions 2 --topic dyingGQ-replicated

# 查看topic主题详情
./bin/kafka-topics.sh --describe --zookeeper 172.0.0.8:2181 --topic dyingGQ-replicated

# 消息生产
./bin/kafka-console-producer.sh --broker-list 172.0.0.10:9092,172.0.0.11:9093,172.0.0.12:9094 --topic dyingGQ-replicated

# 消息消费
./bin/kafka-console-consumer.sh --bootstrap-server 172.0.0.10:9092,172.0.0.11:9093,172.0.0.12:9094 --from-beginning --topic my-replicated-topic

Kafka 支持对每个 partition 做备份,可以将 partition 备份到不同的 broker 上,其中 Leader partition 负责写,剩余 follower 负责同步,当 Leader 宕机后会重新选举出新 Leader。

测试一下 Kafka 集群的使用:

  1. 创建测试主题 TestTopic,并同步到其它 broker上:
    在 kafka 节点上执行 ./bin/kafka-topics.sh --create --zookeeper 172.0.0.8:2181 --replication-factor 3 --partitions 5 --topic TestTopic,它会生成5个分区,3个副本节点。其中2个 Broker 得到2个分区,1个 Broker 得到1个分区。

  1. 集群同步验证
    打开其它 Broker 节点,查看 TestTopic 主题信息:

  1. 集群消息消费验证
    在 broker0上运行一个生产者,broker1、broker2 上分别运行消费者:
    这种方式消费时存在一些问题,初步估计可能是因为多个容器间网络互通问题。故采用下面方式部署 kafka 集群。

基于 docker-compose 部署 kafka 集群

https://github.com/wurstmeister/kafka-docker/blob/master/docker-compose-single-broker.yml

https://blog.csdn.net/noaman_wgs/article/details/103757791

https://blog.csdn.net/java_wxid/article/details/121367036

  1. 安装下载 docker-compose

自己百度

  1. 创建自定义 docker-compose.yml 文件
    /opt/docker/docker_compose 目录下创建 docker-compose-kafka-cluster.yml 文件,文件内容如下:
version: '3.7'
  # 这里使用自定义内网不行,很奇怪
  #networks:
  #    mynet1:
  #       ipam:
  #          config:
  #          - subnet: 172.0.0.0/16

services:
  zookeeper:
    image: wurstmeister/zookeeper
    container_name: zookeeper
    ports:
      - "2181:2181"
        # networks:
        #mynet1:
        # ipv4_address: 172.0.0.8
  kafka:
    image: wurstmeister/kafka
    ports:
      - "9092:9092"
    environment:
      KAFKA_ADVERTISED_HOST_NAME: 192.168.0.13
      KAFKA_CREATE_TOPICS: TestComposeTopic:4:3
      KAFKA_ZOOKEEPER_CONNECT: 192.168.0.13:2181
      KAFKA_BROKER_ID: 1
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://192.168.0.13:9092
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
    container_name: kafka01
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    
  kafka2:
    image: wurstmeister/kafka
    ports:
      - "9093:9093"
    environment:
      KAFKA_ADVERTISED_HOST_NAME: 192.168.0.13
      KAFKA_ZOOKEEPER_CONNECT: 192.168.0.13:2181
      KAFKA_BROKER_ID: 2
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://192.168.0.13:9093
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9093
    container_name: kafka02
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock


  kafka3:
    image: wurstmeister/kafka
    ports:
      - "9094:9094"
    environment:
      KAFKA_ADVERTISED_HOST_NAME: 192.168.0.13
      KAFKA_ZOOKEEPER_CONNECT: 192.168.0.13:2181
      KAFKA_BROKER_ID: 3
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://192.168.0.13:9094
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9094
    container_name: kafka03
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

该文件中一些字段的含义:

  • version:compose 的语法版本;
  • services:要启动的服务;
  • image:服务使用的镜像;
  • container_name:启动后的容器名称;

kafka 相关的配置信息如下:

  • KAFKA_ADVERTISED_HOST_NAME:broker 的主机IP;
  • KAFKA_CREATE_TOPICS:启动时默认创建的Topic,TestComposeTopic:4:3 表示名称为 TestComposeTopic,分区数为4,副本数为3;
  • KAFKA_ZOOKEEPER_CONNECT:连接 zookeeper;
  • KAFKA_BROKER_ID:当前 broker 的Id号;
  • KAFKA_ADVERTISED_LISTENERS:消费者消费消息的IP、Port;
  • KAFKA_LISTENERS:允许消费的IP来源;
  • volumes:文件挂载。

执行 docker-compose -f docker-compose-kafka-cluster-broker.yml up 启动 kafka 集群,启动后进入 kafka01 容器查看 topic 信息如下,说明该 topic 成功同步到了各 broker 节点上:

分别进入各 broker节点,kafka01 当作消息生产者、kafka02 当作消息消费者、kafka03 当作消息消费方,进行消息的发送和接收验证:

# kafka01 生产者
./bin/kafka-console-producer.sh --broker-list 192.168.0.13:9092 --topic TestComposeTopic

# kafka02 消费者
./bin/kafka-console-consumer.sh --bootstrap-server 192.168.0.13:9093 --topic TestComposeTopic --from-beginning

# kafka03 消费者
./bin/kafka-console-consumer.sh --bootstrap-server 192.168.0.13:9094 --topic TestComposeTopic --from-beginning


消费模式

单consumer接收 + 同一个consumer处理

我们可以为每一个topic创建一个Kafka Consumer实例,该 Kafka Consumer 实例用于获取对应topic的消息,并且消息的处理逻辑也由该Consumer实例去处理,这样就将消息的获取与处理放在同一个线程当中,每个线程完整地执行消息的获取与处理。

优点:

  • 每个线程使用自己专属的 Kafka Consumer,由于一个分区只能被一个 Consumer 消费,这样就能保证分区内的消息的消费顺序;

缺点:

  • 每个线程维护自己的 Kafka Consumer 实例,这样会占用较多的系统资源;
  • 每个线程完整地执行消息的获取和处理,一旦消息处理逻辑过重会影响后续消息的处理。
  1. 生成不同 Topic 对应的消费者 MyKafkaConsumerFactory 工厂类
public class MyKafkaConsumerFactory {

    private KafkaConsumer consumer = null;

    private MyKafkaConsumerFactory() {}


    private static class SingletonHolder {
        private static final MyKafkaConsumerFactory INSTANCE = new MyKafkaConsumerFactory();
    }


    public static final MyKafkaConsumerFactory getInstance() {
        return SingletonHolder.INSTANCE;
    }

    // 根据主题生成消费者
    public KafkaConsumer createConsumer(List<String> topics) {
        consumer = new KafkaConsumer(defaultConsumerConfig());
        subscribeTopics(topics);
        return consumer;
    }

    // 根据主题、配置生成消费者
    public KafkaConsumer createConsumer(List<String> topics, Map<String, Object> propsMap) {
        consumer = new KafkaConsumer(propsMap);
        subscribeTopics(topics);
        return consumer;

    }

    // 默认的消费者配置
    private Map<String, Object> defaultConsumerConfig() {
        Map<String, Object> propsMap = new HashMap<>();
        propsMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.0.13:9092");
        propsMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        propsMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "100");
        propsMap.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "10000");
        propsMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        propsMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        propsMap.put(ConsumerConfig.GROUP_ID_CONFIG, "kafka-client-demo");
        propsMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
        return propsMap;
    }

    // 订阅所有主题
    private void subscribeTopics(List<String> topics) {
        consumer.subscribe(topics);
    }

}
  1. 不同 Topic的消息接收处理抽象类
@Slf4j
public abstract class AbstractKafkaConsumer extends Thread implements InitializingBean{

    protected KafkaConsumer consumer;
    private final AtomicBoolean closed = new AtomicBoolean(false);

    @Override
    public void run() {
        try {
            while (!closed.get()) {

                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(10000));
                for (ConsumerRecord<String, String> record : records) {
                    process(record);
                }
            }
        } catch (Exception e) {
            log.error("Kafka consumer poll records occur terrible error!, error msg:", e);
            if (!closed.get()) {
                throw e;
            }
        } finally {
            consumer.close();
        }
    }

    protected abstract void process(ConsumerRecord<String, String> record);

    private void shutdown() {
        if (closed.compareAndSet(false, true)) {
            this.interrupt();
            consumer.wakeup();
        }
    }
}
  1. 具体到某一 Topic的消息处理类
@Component
@Slf4j
public class OrderRefundConsumer extends AbstractKafkaConsumer {

    private static final String TOPIC = KafkaTopicConstant.ORDER_REFUND_TOPIC;

    @Resource
    private KafkaMsgProcessorManager msgProcessorManager;

    @Override
    protected void process(ConsumerRecord<String, String> record) {
        log.info("OrderRefundConsumer receive kafka msg.");
        msgProcessorManager.process(record);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        consumer = MyKafkaConsumerFactory.getInstance().createConsumer(Arrays.asList(TOPIC));
        this.start();
    }
}
@Component
@Slf4j
public class OrderPayConsumer extends AbstractKafkaConsumer {

    private static final String TOPIC = KafkaTopicConstant.ORDER_PAY_TOPIC;

    @Resource
    private KafkaMsgProcessorManager msgProcessorManager;

    // 具体的消息处理方法依赖于 KafkaMsgProcessorManager 管理器
    @Override
    protected void process(ConsumerRecord<String, String> record) {
        log.info("OrderPayConsumer receive kafka msg.");
        msgProcessorManager.process(record);
    }


    @Override
    public void afterPropertiesSet() throws Exception {
        consumer = MyKafkaConsumerFactory.getInstance().createConsumer(Arrays.asList(TOPIC));
        this.start();
    }
}
  1. Kafka消息处理器接口
public interface KafkaMsgProcessor {

    /**
     * 获取消息topic类型
     *
     * @return
     */
    String getTopic();

    /**
     * 消息校验
     *
     * @param kafkaMsg
     * @return
     */
    boolean checkMsg(KafkaMsg kafkaMsg);

    /**
     * 处理Kafka消息逻辑
     *
     * @param kafkaMsg
     */
    void processMsg(KafkaMsg kafkaMsg);
}
  1. Kafka消息处理器实现类
@Component("kafkaMsgProcessorManager")
@Slf4j
public class KafkaMsgProcessorManager {

    @Autowired
    private List<KafkaMsgProcessor> kafkaMsgProcessorList;

    // 主题到处理器的映射
    private static volatile Map<String, KafkaMsgProcessor> processorMap = new HashMap<>();
    // 主题名称集合
    private Set<String> topics = new HashSet<>();

    @PostConstruct
    public void initProcessors() {
        if (processorMap.size() <= 0) {
            kafkaMsgProcessorList.stream().forEach(processor -> {
                String topic = processor.getTopic();
                processorMap.put(topic, processor);
                topics.add(topic);
            });
        }
    }

    /**
     * 获取处理消息的processor
     * @param topic
     * @return
     */
    public KafkaMsgProcessor getProcessor(String topic) {
        if (processorMap.size() <= 0) {
            return  null;
        }
        return processorMap.get(topic);
    }


    public void process(ConsumerRecord<String, String> record) {
        if (record == null) {
            return;
        }

        String topic = record.topic();
        String key = record.key();
        String value = record.value();

        KafkaMsgProcessor processor = getProcessor(topic);
        if (processor == null) {
            log.error("There is no suitable processor for topic:{}", topic);
            return;
        }

        KafkaMsg kafkaMsg = KafkaMsg.buildKafkaMsg(topic, key, value);
        if (!processor.checkMsg(kafkaMsg)) {
            log.error("KafkaProcessor[{}] check msg:{} failed", processor, kafkaMsg);
            return;
        }

        try {
            processor.processMsg(kafkaMsg);
        } catch (Exception e) {
            log.error("Invoke KafkaProcessor[{}] process msg:{} occur error: ", processor, kafkaMsg, e);
        }
    }

    public Set<String> getTopics() {
        return topics;
    }
}
posted @   Stitches  阅读(76)  评论(0编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示