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
Docker 部署 zookeeper
- 创建子网
因为 kafka 集群需要基于 zookeeper 注册中心实现部署,kafka 和 zookeeper 均使用 docker 部署,为了保证 docker 容器间的网络互通,先创建 docker 的自定义子网。
# 创建子网,桥接模式
docker network create --driver bridge --subnet 172.0.0.0/16 dk_network
# 查看已经存在的网络
docker network ls
- 创建文件挂载
mkdir -p /opt/docker/zookeeper_cluster/conf
mkdir -p /opt/docker/zookeeper_cluster/data
mkdir -p /opt/docker/zookeeper_cluster/log
- 安装 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
- 重启 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
子网上面已经创建完了,可以直接使用:
- 安装 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
- 重启 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 集群的使用:
- 创建测试主题 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个分区。
- 集群同步验证
打开其它 Broker 节点,查看 TestTopic 主题信息:
- 集群消息消费验证
在 broker0上运行一个生产者,broker1、broker2 上分别运行消费者:
这种方式消费时存在一些问题,初步估计可能是因为多个容器间网络互通问题。故采用下面方式部署 kafka 集群。
基于 docker-compose 部署 kafka 集群
https://github.com/wurstmeister/kafka-docker/blob/master/docker-compose-single-broker.yml
- 安装下载 docker-compose
自己百度
- 创建自定义 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
实例,这样会占用较多的系统资源; - 每个线程完整地执行消息的获取和处理,一旦消息处理逻辑过重会影响后续消息的处理。
- 生成不同 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);
}
}
- 不同 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();
}
}
}
- 具体到某一 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();
}
}
- Kafka消息处理器接口
public interface KafkaMsgProcessor {
/**
* 获取消息topic类型
*
* @return
*/
String getTopic();
/**
* 消息校验
*
* @param kafkaMsg
* @return
*/
boolean checkMsg(KafkaMsg kafkaMsg);
/**
* 处理Kafka消息逻辑
*
* @param kafkaMsg
*/
void processMsg(KafkaMsg kafkaMsg);
}
- 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;
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步